Java数据结构初阶——反射、枚举以及Lambda表达式
接下来博主会持续更新JavaSE、Java数据结构、MySQL、JavaEE、微服务、Redis等等内容的知识点整理。后续我也会精心制作算法解析、项目经验系列内容,内容绝对干货。相信这些文章能够成为我和大家的“葵花宝典”,喜欢的话就关注一下吧!敬请期待!
反射
定义
- Java的反射( reflection)机制是在运行状态中,对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,同时也可以修改部分类型信息;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射( reflection)机制。
怎么理解反射呢?可以将其看做“照妖镜”,照出了程序的“原形”(类和对象的属性和方法)
在以后的第三方应用开发过程中,可能经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法 。
反射相关的类

Class类
Class类与JVM解析的.class文件
-
Java文件被编译后,生成了.class文件, JVM此时就要去解读 .class文件 ,被编译后的 .class文件也被JVM解析为一个对象,这个对象就是
java. lang.Class -
这样当程序在运行时,每个java文件就最终变成了Class类对象的一个实例。这时我们就可以通过Java的反射机制应用到这个实例,就可以去获得甚至去添加改变这个类的属性和动作,使得这个类成为一个动态的类 。
Class类中的相关方法
常用获得类相关的方法

常用获得类中属性相关的方法
以下方法返回值为Field相关

获得类中构造器相关的方法
以下方法返回值为Constructor相关

获得类中方法相关的方法
以下方法返回值为Method相关

反射的代码实现
获得Class对象的三种方式
- 在反射之前,我们需要做的第一步就是先拿到当前需要反射的类的Class对象,然后就可以使用Class对象中的方法,达到反射的目的。
- 使用 Class.forName(“类的全路径名”);
前提:已明确类的全路径名(也就是哪个包里的哪个类),这也是最常用的一种方法。
- 使用 .class 方法。
说明:仅适合在编译前就已经明确要操作的Class
- 使用类对象的 getClass() 方法示
代码实现
被执行反射操作的一个示例类:
class Student{
//私有属性name
private String name = "bit";
//公有属性age
public int age = 18;
//不带参数的构造方法
public Student(){
System. out.println("Student()");
}
private Student(String name, int age) {
this. name = name;
this. age = age;
System. out.println("Student(String, name) ");
}
private void eat(){
System. out.println("i am eat");
}
public void sleep(){
System. out.println("i am pig");
}
private void function(String str) {
System. out.println(str);
}
@Override
public String toString() {
return "Student{" +"name='" +
name + '\'' +", age=" + age +
'}';
}
}
获得Class对象代码实现及解析:
public class Test1 {
public static void main(String[] args) {
//1.通过getClass()方法获取Class对象
Student student=new Student();
Class<?> c1=student.getClass();
/*
2.直接通过 类名.class 的方式得到,该方法最为安全可靠,程序性能更高
这说明任何一个类都有一个隐含的静态成员变量 class
*/
Class<?> c2=Student.class;
/*
3、通过 Class 对象的 forName() 静态方法来获取,用的最多,
但可能抛出 ClassNotFoundException 异常
*/
Class<?> c3=null;//c3定义放外面,否则就是局部变量了
try {
c3=Class.forName("Student");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
/*
一个类在 JVM 中只会有唯一个 Class 实例,即我们对上面获取的
c1,c2,c3是一样的Class实体
*/
System.out.println(c1.equals(c2));
System.out.println(c1.equals(c3));
System.out.println(c2.equals(c3));
//那么c1,c2,c3经过equals()比较,输出的也都是true
}
}
反射相关方法的使用
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Test2 {
//通过反射创建对象
public static void reflectionNewInstance(){
try {
Class<?> aClass=Class.forName("Student");//获取类的实体
Student student= (Student) aClass.newInstance();//创建一个Student类的实例
System.out.println(student);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
//通过反射得到私有构造方法
public static void reflectionPrivateConstructor(){
try {
Class<?> aClass=Class.forName("Student");//获取类的实体
Constructor<?> constructor=aClass.getDeclaredConstructor(String.class,int.class);
/*
getDeclaredConstructor()参数解释:该方法参数使用指定想要获取的构造方法的参数列表,
但是要注意getDeclaredConstructor()传入参数的写法
*/
//访问私有的构造方法:
/*
正常情况下,Java 的访问修饰符(private, protected, public)会阻止你在类的外部调用私有构造方法。
反射机制虽然能定位到这些私有成员,但默认也遵守这个规则,直接调用会抛出 IllegalAccessException。
*/
constructor.setAccessible(true);//设置访问权限,让我们可以访问到这些私有成员
//调用构造方法
Student student= (Student) constructor.newInstance("Tom",19);
//可以发现反射的构造方法和JavaSE语法一样,创建实例化对象就是默认在调用构造方法
System.out.println(student);//输出
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException |
InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
//通过反射得到私有属性
public static void reflectionPrivateField(){
try {
Class<?> aClass=Class.forName("Student");//获取类的实体
Field field=aClass.getDeclaredField("name");//注意:Field是一个非泛型类
field.setAccessible(true);//设置访问权限
Student student=(Student) aClass.newInstance();
field.set(student,"Peter");//set()方法的参数为实例化对象和修改后的值
System.out.println(student);
} catch (ClassNotFoundException | NoSuchFieldException | InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
//通过反射得到私有方法
public static void reflectionPrivateMethod(){
try {
Class<?> aClass=Class.forName("Student");//获取类的实体
Method method=aClass.getDeclaredMethod("function", String.class);//获得指定方法
method.setAccessible(true);
Student student=(Student) aClass.newInstance();
method.invoke(student,"function....");//调用方法
} catch (ClassNotFoundException | NoSuchMethodException |
InstantiationException | IllegalAccessException |
InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
// reflectionNewInstance();//通过反射创建对象
// reflectionPrivateConstructor();//通过反射得到私有构造方法
// reflectionPrivateField();//通过反射得到私有属性
// reflectionPrivateMethod();//通过反射得到私有方法
}
}
反射性能总结
优点:
- 对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法。
- 增加程序的灵活性和扩展性,降低耦合性,提高自适应能力。
- 反射已经运用在了很多流行框架如: Struts、 Hibernate、 Spring 等等。
缺点:
- 使用反射会有效率问题。会导致程序效率降低。
- 反射技术绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂 。
枚举
什么时候适合用枚举类?
- 只读,不需要修改
比如,创建一个季节类,那么这个类的对象是固定的(春夏秋冬),这个时候用枚举类可以更方便地解决。 - 需要在代码里“语义化”表示,而不是用数字/字符串
比如,不用写:if (status = = 1),而是if (status == OrderStatus.PAID)
可读性、可维护性强很多。 - 需要类型安全,避免传错值
用 int / String 时,别人可以传任何数字/字符串,编译不报错。
用枚举,只能传枚举里定义的实例,编译器直接帮我们校验。 - 需要给每个“常量”附加属性和行为
每个枚举值可以有自己的字段(code、颜色、图标等)。
可以写方法,每个枚举值可以重写行为(策略模式)。
枚举典型使用场景举例
- 状态/类型定义
public enum OrderStatus {
CREATED, PAID, SHIPPED, COMPLETED, CANCELLED;
/*以上这些都是该枚举类的实例化对象,它们可以直接作为switch语句、if()语句的参数。
比如,if(status==CREATED)
*/
}
- 带属性的枚举(常用)
public enum OrderStatus {
//这里的括号里传的就是“构造参数”
CREATED(0, "新建"),
PAID(1, "已支付"),
SHIPPED(2, "已发货"),
COMPLETED(3, "已完成"),
CANCELLED(4, "已取消");
//而且这些枚举对象的定义只能写在代码的最开头,之后再是它们的属性或者是其他方法等等
// 每个枚举对象都有的属性
private final int code;
private final String desc;
//也可以写一些共有的方法
……
// 构造方法(private,只能内部用)
OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
// getter...(可以提供get方法,但是不要提供set方法。)
}
- 枚举的构造方法默认是 private 的,在外面写 new OrderStatus() 是编译报错的,这样也就确保了枚举的实例化对象及对象的属性是固定的。
- 枚举对象不同行为构建示例及解析:
// 1. 定义一个枚举类,名字叫 Calculator(计算器)
public enum Calculator {
// 2. 直接定义枚举的实例化对象,每个实例都需要实现下面的抽象方法,写法是这样的:
ADD { // 加法
@Override
public int apply(int a, int b) {
return a + b; // 加法逻辑
}
},
SUB { // 减法
@Override
public int apply(int a, int b) {
return a - b; // 减法逻辑
}
};
//当然上面这两个实例也可以像上述举例2中一样带上构造参数,调用构造方法
// 每个枚举对象都有的属性
private final int code;
private final String desc;
// 3. 我们可以在类中定义抽象方法
// 意思是:凡是属于 Calculator 枚举的实例,都必须实现这个方法,这样就使得每个对象都可以有自己的行为了
public abstract int apply(int a, int b);
// Getter 方法(是每个对象共有的)
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
使用示例:
public class Test {
public static void main(String[] args) {
// 1. 调用它的方法,我要做加法,就拿出 ADD 这个枚举
int result1 = Calculator.ADD.apply(5, 3);
System.out.println(result1); // 输出 8
// 2. 我要做减法,就拿出 SUB 这个枚举
int result2 = Calculator.SUB.apply(5, 3);
System.out.println(result2); // 输出 2
}
//2.获取它的属性(参数)
System.out.println("编码: " + Calculator.ADD.getCode()); // 输出 1
System.out.println("描述: " + Calculator.ADD.getDesc()); // 输出 加法
}
}
当然,如果使用 == 对枚举类中定义的对象之间进行比较,比较的肯定还是对象地址。
注意:
-
所有我们定义的枚举类都隐式继承自 java.lang.Enum 这个类
-
由于所有枚举类已经隐式继承 Enum 这个类了,enum 关键字定义的枚举类不能再继承其他类了。而Java是单继承机制,可以一层一层地继承(B extends A,C extends B),但不能同时继承多个类。
-
但是枚举类和普通类一样可以实现(implements)多个接口。
枚举类对象常用方法

使用示例及解析:
public enum TestEnum {
RED, BLACK,GREEN,WHITE;
public static void main(String[] args) {
//
TestEnum[] testEnum2 = TestEnum.values();
for (int i = 0; i < testEnum2 . length; i++) {
//输出每个枚举常量对应的索引
System. out.println(testEnum2[i] + "
" + testEnum2[i] . ordinal());
}
//将传入的一个普通字符串通过valueOf()方法转化为一个与字符串名字相同的枚举对象
System. out.println(TestEnum.valueOf
("GREEN"));
System. out.println(BLACK. compareTo
(RED));//输出BLACK的索引与RED索引的差值(o1-o2)
}
枚举和反射
枚举是否可以通过反射,拿到实例对象呢?
- 我们在反射使用的示例中看到了,任何一个类,哪怕其构造方法是私有的,我们也可以通过反射拿到他的实例对象,那么枚举的构造方法也是私有的,我们是否可以拿到呢?
Class<?> aClass = Class.forName("TestEnum");
//注意传入对应的参数,获得对应的构造方法来构造对象,假设TestEnum枚举类构造方法是提供了两个参数分别是int和String。
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, String.class);
如果使用以上的方式来获取此枚举类的构造方法,程序会抛出异常,异常信息是: java. lang. NoSuch MethodException: TestEnum. <init>(java. lang. String, int)
意思是:没有对应的构造方法!
我们提供的枚举的构造方法就是两个参数分别是 int 和 String 啊?问题出现在哪里呢?
- 枚举比较特殊,虽然我们提供的构造方法的参数是两个,但是默认它还添加了两个参数。
我们所有的枚举类,都是默认继承于 java. lang. Enum 。那么子类就要帮助父类进行构造,枚举就在子类的构造方法中默认添加了父类构造方法的参数。
哪两个参数呢?我们看一下Enum类的源码:
protected Enum(String name, int ordinal) {
this.name= name;
this.ordinal = ordinal;
}
编译器会自动在你写的枚举类构造方法的所有参数前面插入 String name, int ordinal 这两个参数,并自动在方法体内部第一行加上 super(name, ordinal);
也就是说,这里正确的参数应该是4个
Constructor<?> constructor = aClass.getDeclaredConstructor(String.class,int.class,
int.class,String.class);
然后再执行程序获得TestEnum的实例
TestEnum testEnum = (TestEnum)constructor. newInstance("父类参数",111,222,"子类参数");
然而执行结果却抛出了java. lang. IllegalArgumentException 异常
没错!还是有问题。但这是正常的问题!
虽然枚举的构造器可以通过反射获取(并设为可访问),但 newInstance() 方法内部有显式检查,如果是枚举类型,则直接抛出该异常。
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
- 这样做是为了维护枚举( enum )的语义完整性,以及确保了每个枚举常量在 JVM 中是单例的。
- 如果允许通过反射任意创建枚举实例,就破坏了这些基本语义,导致程序行为不确定。
这个问题也是个非常重要的面试题,原版问题是:
为什么枚举实现单例模式是安全的?
Lambda表达式
Lambda表达式的基本语法
基本语法:
(parameters) -> expression
或
(parameters) ->{ statements; }
Lambda表达式由三部分组成:
- paramaters:类似方法中的形参列表,这里的参数是函数式接口里的参数。
- -> :可理解为 “被用于”的意思
- 方法体:可以是表达式也可以代码块,是函数式接口里方法的实现,这里的代码块等同于方法的方法体。
- Lambda 表达式就是专门为函数式接口设计的,这是 Java 8 引入 Lambda 时最核心的设计思想。
那什么是函数式接口?
函数式接口
函数式接口(Functional Interface)是指:
接口里有且仅有一个抽象方法(可以有其他非抽象方法:默认方法、静态方法、私有方法)
通常会用 @FunctionalInterface 注解显式标记,使用注解可以让编译器帮助我们自动检查是否实现为函数式接口,这样如果写了两个抽象方法,程序编译就会报错的。
@FunctionalInterface
interface NoParameterNoReturn {
void test();
}
Lambda表达式的基本使用
有一下几种函数式接口,针对每个接口使用Lambda表达式来创建接口对象。
//接口内抽象方法无返回值无参数
@FunctionalInterface
interface NoParameterNoReturn {
void test();
}
//接口内抽象方法无返回值一个参数
@FunctionalInterface
interface OneParameterNoReturn {
void test(int a);
}
//接口内抽象方法无返回值多个参数
@FunctionalInterface
interface MoreParameterNoReturn {
void test(int a,int b);
}
//接口内抽象方法有返回值无参数
@FunctionalInterface
interface NoParameterReturn {
int test();
}
//接口内抽象方法有返回值一个参数
@FunctionalInterface
interface OneParameterReturn {
int test(int a);
}
//接口内抽象方法有返回值多个参数
@FunctionalInterface
interface MoreParameterReturn {
int test(int a,int b);
}
我们针对前4个接口使用Lambda表达式来创建接口对象,并进行相关解析:
public static void main1(String[] args) {
NoParameterNoReturn noParameterNoReturn= ()->System.out.println("重写test方法!!");//方法体内只有一条语句,可以省略大括号{}
noParameterNoReturn.test();
}
public static void main2(String[] args) {
//OneParameterNoReturn oneParameterNoReturn = (int a) -> {System.out.println(a);};
//下面进行简化:
OneParameterNoReturn oneParameterNoReturn = a -> System.out.println(a);
/*
在绝大多数情况下,Lambda 表达式的参数类型都可以省略。参数只有一个的话,甚至小括号 () 都可以省略。
这是 Java 编译器的类型推断(Type Inference)功能在起作用。
原因是:编译器会根据 Lambda 实现的函数式接口的抽象方法定义,自动推断出每个参数的类型。
*/
oneParameterNoReturn.test(10);
}
public static void main3(String[] args) {
//MoreParameterNoReturn moreParameterNoReturn = (int a,int b) -> {System.out.println(a+b);};
//下面进行简化:
MoreParameterNoReturn moreParameterNoReturn = (a,b) -> System.out.println(a+b);
moreParameterNoReturn.test(1,2);
}
public static void main4(String[] args) {
//NoParameterReturn noParameterReturn = ()->{return 10;};
//下面进行简化:
NoParameterReturn noParameterReturn = ()->10;
//我们知道了方法体内只有一条语句,可以省略大括号{},但如果这条语句是 return 语句的话,就必需要把return也一起去掉 System.out.println(noParameterReturn.test());
}
Lambda表达式和匿名内部类
两者的共同本质
Lambda表达式可以理解为是匿名内部类的简化,他们实现的过程实际上都是可以理解为:创建了一个实例、实现了接口、重写了接口的方法 。
不使用lambda表达式的时候的直接创建接口对象的方法(使用匿名内部类):
//这里使用建堆的例子:为PriorityQueue传入比较器,也就是传入Comparator接口的实例(Comparator也是一个函数式接口):
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println(a);
return o1.compareTo(o2);
}
});
使用Lambda表达式直接创建接口对象:
PriorityQueue<Integer> priorityQueue2 =
new PriorityQueue<>( (o1,o2)->o1.compareTo(o2) );
- 匿名内部类 和 Lambda 表达式,本质上都是在“创建一个对象”,这个对象实现了某个接口(或继承了某个类)。
- 那么对于接收Lambda表达式的引用来说,它就理所当然既可以调用重写的抽象方法,也可以调用其他接口中存在的非抽象方法。
变量捕获
Lambda表达式和匿名内部类中都存在变量捕获的现象。
-
匿名内部类的生命周期可能比外部方法更长。当外部方法执行完毕,栈帧销毁,那局部变量就没了。但如果匿名内部类还在(比如被线程使用),它需要继续使用那个变量的值。
-
所以,Java 做了一个拷贝动作:把匿名内部类外中所要用到的变量的值拷贝一份放到匿名内部类的对象里。这就是“捕获”。
-
而被捕获的变量,从它被定义开始就不能再被更改了,在匿名内部类里面还是外面任何地方都不行。(当然如果被捕获的本身就是常量,那本来也就不能修改)
public static void main(String[] args) {
int a = 10;
//a = 9;这里会报错,因为a已经被Comparator捕获,不能有任何的修改了
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println(a);//匿名内部类内使用,a会被捕获
return o1.compareTo(o2);
}
});
}
那么对于Lambda表达式也一样:
public static void main(String[] args) {
int count = 10;
MoreParameterNoReturn moreParameterNoReturn = (int a,int b) -> {
System.out.println(count);
System.out.println(a+b);
};
//count = 99; 报错,因为count已经被MoreParameterNoReturn捕获,
//它在任何地方都不能再修改了
}
为什么不能改?
- 因为内部类持有的是变量的副本。如果你在外部改了,内部的副本不会同步,会造成数据不一致,所以 Java 直接禁止了这种行为。
Lambda表达式在集合当中的使用
Lambda表达式与集合类的结合
- 为了能够让Lambda表达式和Java的集合类更好的一起使用,集合当中,也新增了部分接口,以便与Lambda表达式对接。

Collection的forEach()方法是从接口
java. lang. Iterable拿过来的。
下面是其中两个方法的使用示例及解析:
forEach() 方法
在List接口中的使用
该方法在接口 Iterable 当中,原型如下:
default void forEach(Consumer<? super T> action) {//传入Consumer函数式接口对象 action
Objects. requireNonNull(action);
for (T t : this) {
action. accept(t);//调用action对象当中的accept()方法,执行对t的指定行为
}
}
使用forEach() 方法:
//1.使用匿名内部类方式:
ArrayList<String> list = new ArrayList<>();
list.add("abcd");
list.add("zko");
list.add("hello");
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {//重写accept方法
System.out.println(s);//对每次遍历到的元素 s 执行指定的行为(打印 s)
}
});
//2.使用Lambda表达式:
list.forEach(s -> System.out.println(s));
在Map接口中的使用
forEach() 方法原型:
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects. requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no l
onger in the map.throw new ConcurrentModificationException(ise);
}
action. accept(k, v);
}
}
使用forEach() 方法:
//1.使用匿名内部类:
HashMap<String,Integer> map = new HashMap<>();
map.put("hello",3);
map.put("abc",5);
map.put("def",2);
map.forEach(new BiConsumer<String, Integer>() {
@Override
public void accept(String s, Integer integer) {
System.out.println("key: "+s +" val: "+integer);
}
});
//2.使用Lambda表达式:
map.forEach((key,val)->System.out.println("key: "+key +" val: "+val));
sort()方法
使用匿名内部类:
list.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
使用Lambda表达式:
list.sort((o1,o2) -> o1.compareTo(o2));
更多推荐
所有评论(0)