前言❤️❤️
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
lambda表达式,反射,枚举其实都是相对不怎么重要,但是完全不学,也不合适,因为面试的时候,也会提到一些,因此这里的内容大家简单了解一下即可,很多东西不需要掌握😊
forEach方法与数据结构是紧密相关的,还没学习数据结构的铁汁,就不需要看
🎇个人主页:洋不写bug的博客
🎇所属专栏:Java学习之旅,从入门到进阶
🎇铁汁们对于Java的各种常用核心语法(不太常用的也有😆),都可以在上面的Java专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~
1,反射
1,简介
所谓反射,就是让一个对象认识自己,这样说可能大家会比较懵逼,那原来对象就就不认识自己了吗???
欸,还真不认识,那谁认识对象呢,程序员认识,程序员非常清楚,
某个对象属于哪个类,这个对象有哪些属性(包括属性的名字,类型,private/public,其他的修饰符注解...)。
这个对象有哪些方法,(方法的名字,参数列表,public/private)
这个对象的父类时谁
这个类实现了哪些接口
程序员之所以知道,是因为程序员可以直接看代码😅
那在程序中,对象怎么获取到这些信息呢,JVM提供了一组API,通过API拿到指定对象的上述信息,那么什么是API呢???
API其实就是一组方法和类,供别人使用,就比如Java的标准库中的各种类,例如Random,Scanner,Set,List,Queue...
那反射到底有什么用呢???好比家里的门,正常是用钥匙去开的,那我们如果想强制开的话,就需要叫开锁师傅了,这样虽然开了,但是就容易把门给搞坏。
反射属于是一种非常规编程手段,我们在实际开发中,要慎用,它是一把双刃剑,我们并不推荐使用。因此这里只是了解即可,不需要像前面那样完全掌握。
反射代码如下,这里就要抛出异常,因为这里没办法确定类名写的是对的,也没办法确定这个类是否允许被访问(类如果是private时,就不允许被访问)。
java
public class Test1 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
//通过反射,获取到 String 这个类的Class对象
Class stringClass = Class.forName("java.lang.String");
Object obj = stringClass.newInstance();
String str = (String) obj;
System.out.println(str);
}
}
反射过程拆分就是下面三步
- 首先拿到这个类(forname方法括号里就是类名);
- 创建出这个类的实例(创建出对象),这时候创建的对象是Object类型;
- 进行类型转化,转为String类型。这里就相当于创建了一个空的字符串
这样就通过反射,创建出了一个空的字符串,可能有的铁汁会说:那我直接像下面这样去创建,一行就OK了,这里这么麻烦,那有啥好处呢?
java
String s = "";
这样写的代码其实是静态的,在编译过程中就知道创建的对象的类型。
而利用反射的写法来写,则是动态的。
在编译时其实编译器并不能确定到底创建了什么类型的对象,要在运行时才能确定,括号里的东西,其实有多种选择,除了事先写好,还可以来自用户从控制台中的输入,还可以从文件读取,还可以通过一系列计算得出,还可以从网络中读取...
java
Class stringClass = Class.forName("java.lang.String");
这个动态就是这里付出这么大的代价获得的好处
动态就有利于代码的通用性,就是一份代码可以支持许多种不同的情况,这就体现在序列化中
序列化就类似于三体中的降维打击,把一个对象转成一个字符串,就是序列化,而把一个字符串转化成一个对象,就是反序列化
如下图,就是把对象转为字符串,那么由于这两个类不同,序列化的规则也是不同的,有了反射,我们就可以大幅降低序列化的成本。
如果我们在写代码之前,并不知道这个类里面的具体信息,那么就可以像下面这样拿到类的信息,但是这种操作我们普通的程序员不会怎么用, 这种操作一般那种底层框架的开发者才会去使用,我们了解下即可
java
Class stringClass = Class.forName("java.lang.String");
2,利用反射绕过权限检查(开锁)
1,开锁变量
下面这个代码一看就会报错,在类中访问其他类的私有属性,是不允许的
java
class Student{
private int id;
private String name;
}
public class Test2 {
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.id);
}
}
但是利用反射,就可以绕过权限检查,来开这个锁。
这里用Class拿到类属性,用Field拿到id属性 (这些关键字一会也会总结)
(这里博主的Student类是写在一个last包中的,获取class对象时需要加上包名)
如下所示:
java
import java.lang.reflect.Field;
class Student{
private int id;
private String name;
}
public class Test2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//获取到Student类的class对象
Class studentClass = Class.forName("last.Student");
//拿到这个类中的id属性
Field idField = studentClass.getDeclaredField("id");
//把id设置为可访问的,就相当于开锁了
idField.setAccessible(true);
Student student = new Student();
//更改id的值
idField.setInt(student,100);
int id = idField.getInt(student);
System.out.println(id);
}
}
拿到属性后把id属性开锁,设置为可访问的
注:这里的开锁指的只是开完后能通过反射的方法访问,正常去创建一个对象,直接去访问id属性,也是不行的
接下来就可以通过反射的方式去修改和获取id的值了,也就开锁成功了
获取Student类的Class如果直接像下面这样写,就失去了动态的价值。
java
//获取到Student类的class对象
// Class studentClass = Class.forName("last.Student");
Class studentClass = Student.class;
2,开锁方法
在Student类中加上这两个private类型的print方法,尝试下开锁方法
开锁方法其实跟前面的开锁变量类似,用Method拿到方法属性,接着把方法属性设置为可访问的,代码如下所示:
java
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class Student{
private int id;
private String name;
private void print(){
System.out.println("id:" + id + "name:" + name);
}
private void print(String prefix){
System.out.println(prefix + "id:" + id + "name:" + name);
}
}
public class Test2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
//获取到Student类的class对象
// Class studentClass = Class.forName("last.Student");
Class studentClass = Student.class;
//拿到这个类中的id属性
Field idField = studentClass.getDeclaredField("id");
//把id设置为可访问的,就相当于开锁了
idField.setAccessible(true);
Student student = new Student();
//更改id的值
idField.setInt(student,100);
int id = idField.getInt(student);
System.out.println(id);
Method printMethod = studentClass.getDeclaredMethod("print", String.class);
printMethod.setAccessible(true);
printMethod.invoke(student,"hello");
}
}
}
}
但是这里就有个问题:用Method拿到方法属性,那两个方法重载,获取到的是哪个方法,如何选择呢?
其实就是看方法里传入的参数,这里有一个String.class,就是调用有一个String参数的方法,如果getDeclaredMethod方法就传入了一个"print",那就是掉用无参方法。
上面的代码中,获取方法时传入了Strng.class,就表示调用有String参数的方法,打印结果如下:
获取类,方法,属性时的类名如下所示:
具体的方法,比如invoke方法,我们完全不用去记忆,要用时直接用AI查就行了(其实基本上也用用不到😅)
不知道铁汁们有没有这样一个问题,类中设置private属性,就是不想让访问呀,用反射绕过权限访问,这跟设计初衷是矛盾的呀!
其实,反射操作打破了类的封装性,可能导致代码安全性问题和逻辑混乱,因此不建议在常规开发中使用。另外,在某些安全管理器(SecurityManager)环境下,这种操作可能会被禁止。
正常开发是几乎不会去使用的,只有写框架的程序员会去使用,后续我们学习库/框架的时候,看到代码知道是什么意思即可。
2,枚举
枚举的英文就是enum,其含义就是可以列举的概念;
例如在代码中用数字表示性别"0" 表示女,用"1"表示男,那么就会出现这样的代码:
java
if(gender == 1){
}
当别人看到代码时,就会不太理解这是什么意思
为了方便别人理解代码,就可以定义两个常量,如下所示:
java
public class Test3 {
public static int FEMALE = 0;
public static int MALE = 1;
public static void main(String[] args) {
int gender = FEMALE;
if(gender == FEMALE){
}else if(gender == MALE){
}
}
}
这样定义两个常量又有个新的问题,就是两个常量相加会等于另一个常量,这明显是逻辑错误,但是编译器也不会报错😅
java
if(FEMALE + MALE == MALE){
}
如果既想让别人能看懂代码,又可以在逻辑错误时让编译器报错,就要使用枚举来解决
创建类时,选择Enum(如下图),创建的类就是枚举类:
创建一个名字为Gender的枚举类,在里面写了三个枚举项,表示性别,中间用逗号隔开(other表示不方便透露的情况),通过Gender.Female和Gender.Male来表示性别
在main方法中再尝试下加法,就会报错,如下所示:
java
public enum Gender {
FEMALE,MALE,OTHER
}

接着再写个表示颜色的枚举类,每个枚举项中还可以加一些属性,例如加颜色的名称和颜色值这两个属性,代码如下所示:
java
public enum Color {
RED("红色",0xff0000),
GREEN("绿色",0x00ff00),
BLUE("蓝色",0x0000ff);
private String name;
private int value;
//构造方法,指定枚举的属性值
Color(String name,int value){
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
}
- 当枚举项下面定义的有方法,变量时,就需要加上分号,BLUE("蓝色",0x0000ff)后面就要加上分号
只有单独的枚举项时,就不需要加(前面的那个性别枚举类就不需要加) - 这里的0xff0000表示的是十六进制的颜色值
- 枚举型中加属性值的话,就要写个构造方法(参数类型要跟枚举项中的属性相对应),否则是会报错的(如下图,把构造方法注释掉后,就会报错)

接着在main方法中使用下,直接打印c.getValue()是十进制,看着不方便,这里通过printf转为十六进制打印,如下所示:
java
public class Test4 {
public static void main(String[] args) {
Color c = Color.RED;
System.out.println(c.getName());
System.out.printf("0x%x",c.getValue());
}
}

注:枚举不能在类外面创建实例(枚举类的构造方法是私有的),也不能被继承
那如果利用前面的反射拿到枚举类的私有构造方法,再在其它类中去创建枚举类的实例,这样可以吗这种做法,其实就类似于故意找茬,Java在开发的时候,就考虑到了这种情况,因此在底层代码中,是不允许通过反射给枚举创建实例的😅
3,lambda表达式
1,简介
在许多编程语言中,都有个匿名函数的设定,但是在Java中,函数是要写在类中的,因此Java中就采取了一种曲线救国的方式,那就是引入functiona interface(函数式接口)。
这个匿名函数通常作为回调函数使用,现在先不着急用,等需要的时候再调用,就好像甄嬛传中一个妃子在另一个妃子旁边安插的眼线。😂在JavaSE阶段提到的Comparable/Comparator就是回调函数。
一个接口里面如果只有一个抽象方法,那这个接口就是函数式接口,那么基于这个函数式接口,就引申出了lambda表达式,也就是Java的匿名方法。
创建个优先级队列,需要指定Student的比较规则,就可以采用匿名内部类的写法,如下所示:
java
import java.util.Comparator;
import java.util.PriorityQueue;
public class Test5 {
static class Student{
public String name;
public int id;
}
public static void main(String[] args) {
//这里我们创建一个优先级队列,需要指定Student的比较规则
PriorityQueue<Student> queue = new PriorityQueue<Student>(new Comparator<Student>(){
@Override
public int compare(Student o1,Student o2){
return o1.id - o2.id;
}
});
}
}
把鼠标箭头放在Comparator处,这里就会出现提示,提示我们可以把这个替换为lambda表达式,点击一下

接着这个匿名内部类就被替换为了lambda表达式
java
public static void main(String[] args) {
//这里我们创建一个优先级队列,需要指定Student的比较规则
PriorityQueue<Student> queue = new PriorityQueue<Student>((o1, o2) -> o1.id - o2.id);
}
}
lambda表达式中的形参类型省略,方法名称和类型也省略,return省略(当 Lambda 体只有一条语句,并且这条语句就是返回值语句时,可以省略
return和分号;)

这个lambda表达式可能刚用会觉得很别扭,但是写代码时间长了,就是真香🐵
2,函数式接口和语法精简
这段代码中,这个导入是灰色的,这是因为 Lambda 表达式的类型推断机制会保证编译器识别出它需要实现 java.util.Comparator 接口,无需显式导入,但我们最好还是写一下。
按ctrl键+鼠标点击上面的Comparator,就会发现这是一个函数式接口。
所谓的函数式接口,其实铁汁们可以简单理解就是一个接口里面只写了一个抽象方法这里我们搞个函数式接口,再创建个匿名内部类来实现下,代码如下所示:
java
@FunctionalInterface
interface MyFuncInterface{
//函数式接口里面只能有一个抽象方法
void print(String s);
}
public class Test6 {
public static void main(String[] args) {
MyFuncInterface myFuncInterface = new MyFuncInterface() {
@Override
public void print(String s) {
System.out.println(s);
}
};
}
}
lambda表达式省略了new MyFuncInterface(),省略了大括号,省略了方法名(因为接口中只有这一个方法,编译器知道重写的一定是这个方法),如下所示:
java
public static void main(String[] args) {
MyFuncInterface myFuncInterface = (s -> System.out.println(s));
myFuncInterface.print("hello world!!!");
}
补充下,这里lambda不简写应该是这样的,
java
MyFuncInterface myFuncInterface = (String s) -> {
System.out.println(s);
};
lambda表达式的语法精简规则如下:
①参数类型可以省略,如果需要省略,每个参数的类型都要省略。
②参数的小括号里面只有一个参数,那么小括号可以省略
③如果方法体当中只有一句代码,那么大括号可以省略
④如果方法体中只有一条语句,且是 return 语句,那么大括号可以省略,且去掉 return 关键字。
lambda表达式还可以作为函数式接口的实参,对方法进行重写,在MyPriorityQueue这个类中,接收这个接口作为参数,main方法中使用Lambda 表达式 (s1, s2) -> s1.compareTo(s2) 作为 MyPriorityQueue 构造方法的参数。
方法体 s1.compareTo(s2)就是用字典序来比较。
java
interface MyPriorityQueueComparator{
int compare(String s1,String s2);
}
class MyPriorityQueue{
//具体的实现在数据结构博客中会有解析
public MyPriorityQueue(MyPriorityQueueComparator comparator){
}
}
public class Test7 {
public static void main(String[] args) {
MyPriorityQueue queue = new MyPriorityQueue(((s1, s2) -> s1.compareTo(s2)));
}
}
还可以直接在return后写lambda表达式,如下图:
lambda表达式的常见使用场景:
- 把 Lambda 赋值给变量(函数式接口变量)
- 把 Lambda 作为方法实参传递进去
- 把 Lambda 作为方法返回值 return 出去
这其实就是函数式编程语言的特点,Java不是函数式编程语言,而是面向对象的语言,在大数据开发时函数式编程比较常见,其他场景则很少见。
3,变量捕获
铁汁们看下面这段代码,main方法运行时会不会报错
java
interface MyFuncInterface2{
void print(String s,int i);
}
public class Catch {
public static void main(String[] args) {
MyFuncInterface2 myFuncInterface2;
{
int num = 100;
myFuncInterface2 = ((s, i) -> System.out.println(num));
}
myFuncInterface2.print("aaa", 2);
}
}
我们就来分析下,如下图,出完红色方框圈住的这个代码块,num这个变量就已经销毁了,后面在lambda表达式中去打印num,因此会报错
执行结果如下图,这里却能正常的打印出来
这是为什么呢?其实这里在定义lambda表达式的时候,lambda就把捕获的变量拷贝了一份,因此打印的是拷贝的num,原来的那份销不销毁都无所谓
那有的铁汁会说:不对呀,万一原来的那份后面修改了,还是打印拷贝的那份,那打印的结果不就错了吗?
那写代码试一下就清楚了,如下:
java
interface MyFuncInterface2{
void print(String s,int i);
}
public class Catch {
public static void main(String[] args) {
int num = 100;
MyFuncInterface2 myFuncInterface2;
{
myFuncInterface2 = ((s, i) -> System.out.println(num));
}
num = 10000;
myFuncInterface2.print("aaa", 2);
}
}
Java的创建者考虑到了这个情况,为了防止有歧义,后面再次修改num时就会报错(如下图)
报错信息翻译就是 Lambda 表达式中使用的外部局部变量,必须是 final 修饰的,或者是 "实质上 final" 的(这里的实质上final,就是指赋值后没有修改过的)
4,forEach方法
如果有一个Integer类型的顺序表,想要把这个顺序表每个数据扩大两倍打印出来
除了写个for循环,还可以使用标准库的forEach方法,这个forEach方法就是用来遍历集合,对集合里每一个元素执行我们指定的操作
forEach方法的形参规定是Comsumer接口,这个接口中有个void类型的accept方法,主要用于接收一个输入参数并对其进行处理那就可以通过匿名内部类的写法,重写accept方法,将元素扩大两倍打印出来
注:参数i就是forEach方法读取集合的元素,一个一个传进去的(这个写在了forEach底层代码中)
代码如下所示:
java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class Test8 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
// 不使用Lambda,通过匿名内部类实现Consumer接口
list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer i) {
// 实现具体逻辑:将元素扩大两倍并打印
System.out.println(i * 2);
}
});
}
}

也可以直接使用lambda表达式,省略Consumer接口,省略accept方法,省略大括号,直接写,这样代码看起来就非常清爽,如下:
java
import java.util.ArrayList;
import java.util.List;
public class Test8 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
//使用foreach循环把链表中的数据扩大两倍打印出来
list.forEach(i -> System.out.println(i * 2));
}
}
结语
这篇博客是JavaSE专栏的最后一篇博客,JavaSE学习之旅这个专栏,到这里就已经完结了
这个专栏里面包含了各种Java常用的基础知识,如果铁汁们发现内容错误,或者有一些知识点没写清楚,都可以私信博主,博主看到后一定回复🐵感谢大家对博主的支持,希望这些博客能够真正的对大家有用💪💪💪
铁汁们后面也可以关注博主的数据库专栏(已经基本上更新完了),JavaEE专栏,算法专栏,数据结构专栏,博主也会尽力的加快速度去更新🐵😊
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉











