绕过权限检查,访问修改私有属性,反射,枚举,lambda

前言❤️❤️

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);
    }
}

反射过程拆分就是下面三步

  1. 首先拿到这个类(forname方法括号里就是类名);
  2. 创建出这个类的实例(创建出对象),这时候创建的对象是Object类型;
  3. 进行类型转化,转为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表达式的常见使用场景:

  1. 把 Lambda 赋值给变量(函数式接口变量)
  2. 把 Lambda 作为方法实参传递进去
  3. 把 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专栏,算法专栏,数据结构专栏,博主也会尽力的加快速度去更新🐵😊

以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

相关推荐
evans在进步23 分钟前
Java 常用设计模式入门:建造者、工厂、单例、外观与代理
java·python·设计模式
lisin-lee-cooper34 分钟前
【leetcode658】有序数组找出k个最接近x的数
java·数据结构·算法
sunburn-42 分钟前
Java堆(Heap)详解与实战教学
java·开发语言·数据结构·ide·算法
Kyrie_kk42 分钟前
Java--TimeUnit时间单位枚举类(时间单位规范)
java·后端
念何架构之路1 小时前
beego总体架构与工程结构
java·架构·beego
典典分享指南1 小时前
用飞书多维表格搭建内容管理看板
java·python·r语言·composer·symfony
东坡白菜2 小时前
windows下使用Trae启动java项目遇到的乱码问题
java
SQL-First布道者2 小时前
⚡Spring JDBC 完整体系 · 第 10 集 · 手搓 `BaseCondition`
java·spring boot·后端·spring·mybatis·spring jdbc
珍珠先生2 小时前
第9章 JDK 11 新特性:var 与便捷新 API
java