Java---反射、枚举、lambda表达式 和 泛型进阶

(一).反射

1.定义

Java的反射机制是在运行 状态中,对于任意一个 ,都能够知道这个类的所有属性和方法 ;对于任意一个对象 ,都能够调用他的任意方法和属性 ,既然能拿到那么,我们就可以修改部分类型信息;这种动态获取信息以及动态调整对象方法的功能称为Java语言的反射机制。

2.用途

(1).在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放 ,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法

(2).反射就重要的用途就是开发各种通用框架,比如在spring中,我们将所有的类Bean交给spring容器管理,无论是XML配置Bean还是注解配置,当我们从容器中获取Bean来依赖注入时,容器会读取配置,而配置中给的就是类的信息,spring根据这些信息,需要创建那些Bean,spring就动态的创建这些类。

3.反射基本信息

Java程序中许多对象在运行时会出现两种类型:运行时类型编译时类型

例如Preson person=new Student();这句代码中p在编译时类型为Person,运行时类型为Student。程序需要在运行时发现对象和类的真实信息。而通过使用反射程序就能判断出该对象和类属于哪些类。

4.反射相关的类

|--------------|----------------------------|
| 类名 | 用途 |
| Class类 | 代表类的实体,在运行的Java引用程序中表示类和接口 |
| Field类 | 代表类的成员变量/类的属性 |
| Method类 | 代表类的方法 |
| Constructor类 | 代表类的构造方法 |

(1).Class类(反射机制的起源)

Class代表类的实体,在运行的Java应用程序中表示类和接口

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

i.常用获得类相关的方法

|---------------------------|----------------------------------|
| 方法 | 用途 |
| getClassLoader() | 获得类的加载器 |
| getDeclaredClasses() | 返回一个数组,数组中包含该类中所有类和接口类的对象(包括私有的) |
| forName(String className) | 根据类名返回类的对象 |
| newInstance() | 创建类的实例 |
| getName() | 获得类的完整路径名字 |

ii.常用获得类中属性相关的方法(返回值为Field相关)

|-------------------------------|-------------|
| 方法 | 用途 |
| getField(String name) | 获得某个公有的属性对象 |
| getFields() | 获得所有公有的属性对象 |
| getDeclaredField(String name) | 获得某个属性对象 |
| getDeclaredFields() | 获得所有属性对象 |

iii.获得类中注释相关的方法

|----------------------------------------------|---------------------|
| 方法 | 用途 |
| getAnnotation(Class annotationClass) | 返回该类中与参数类型匹配的公有注释对象 |
| getAnnotations() | 返回该类所有的公有注释对象 |
| getDeclaredAnnotation(Class annotationClass) | 返回该类中与参数类型匹配的所有注释对象 |
| getDeclaredAnnotations() | 返回该类所有的注释对象 |

iv.获得类中构造器相关的方法(返回值为 Constructor相关)

|------------------------------------------------------|---------------------|
| 方法 | 用途 |
| getConstructor(Class...<?> parameterTypes) | 获得该类中与参数类型匹配的公有构造方法 |
| getConstructors() | 获得该类的所有公有构造方法 |
| getDeclaredConstructor(Class...<?> parameterTypes) | 获得该类中与参数类型匹配的构造方法 |
| getDeclaredConstructors() | 获得该类所有的构造方法 |

v.获得类中方法的相关方法(返回值为 Method相关)

|-------------------------------------------------------------|-------------|
| 方法 | 用途 |
| getMethod(String name,Class...<?> parameterTypes) | 获得该类某个公有的方法 |
| getMethods() | 获得该类所有公有的方法 |
| getDeclaredMethod(String name,Class...<?> parameterTypes) | 获得该类某个方法 |
| getDeclaredMethods() | 获得该类所有方法 |

(2).反射示例

1.获取Class对象的三种方式

在反射之前,我们需要做的第一步就是先拿到当前需要反射的类的Class对象 ,然后通过Class对象的核心方法,达到反射的目的,即:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法 ;对于任意一个对象,都能够调用它的任意方法和属性,既然能拿到,那么我们就可以修改部分类型信息。

i.使用Class.forName("类的全路径名");静态方法。

前提:已明确类的全路径名

ii.使用.class方法。

仅适合在编译前就已经明确要操作的Class

iii.使用类对象的getClass()方法
java 复制代码
package demo1;


class Student{
    private String name="byte";
    private int age=30;

    public Student(){
        System.out.println("Student()");
    }
    private Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    private void eat(String s){
        System.out.println(s+"正在吃饭");
    }
    private void sleep(){
        System.out.println("Sleep()");
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class Test {

    public static void main(String[] args) {
        try {
            //使用Class.forName("类的全路径名");静态方法
            Class<?> c1=Class.forName("demo1.Student");
            //使用.class方法。
            Class c2=Student.class;
            //使用类对象的getClass()方法
            Student student=new Student();
            Class<?> c3=student.getClass();
            System.out.println(c1==c2);
            System.out.println(c3==c2);
            System.out.println(c1==c3);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

通过上面的图片我们可以看到,代码的运行全是"true",说明一个类在JVM中只会有一个Class实例

2.反射的使用

依旧是反射上面的Student类,把反射的逻辑写到另外的类当中进行理解

注意:所有和反射相关的包都在import.java.lang.reflect包下面。

i.通过反射创建对象
ii.反射私有的构造方法

其中

java 复制代码
            //解除访问权限检查(突破private限制)
            //所有私有的方法,都要经过确认,即需要打一个标记,
            //即已经知道这个权限是私有的,可以进行修改
            constructor.setAccessible(true);

这句话的作用就是知道带参数的构造方法是私有方法了,可以进行修改,如果我将这行代码注释掉,程序就会报错

IllegalAcessException表示我们访问的类成员的构造方法是私有的,报异常说明没有这个权限,要想解决我们就要加上**constructor.setAccessible(true);**这句代码

iii.反射私有属性
iv.反射私有方法

invoke原码

5.反射的优缺点

优点:

①.对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法

②.增加程序的灵活性和扩展性,降低耦合性,提高自适应能力

③.反射已经运用在了很多流行框架中,如:Struts、Hibernate、Spring等等。

缺点:

①.使用反射会有效率问题。会导致程序效率降低

②.反射技术绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂

(二).枚举

1.背景及定义

枚举是在JDK1.5以后引入的,主要用途是:定义一组具有固定取值范围的常量,同时赋予这些常量 "类型" 的特性 。它不只是简单地把常量 "组织起来",更是为了解决传统常量定义(如public static final)的缺陷,让常量具备类型安全、可枚举、可扩展的特性。

java 复制代码
public enum enumTest {
    RED,BLACK,BLUE,WHITE;
}

本质上是java.lang.Enum的子类,也就是说,自己写的枚举类,就算没有显示的继承Enum,但是其默认继承了这个类。

优点:将常量组织起来统一进行管理

缺点:错误状态码,消息类型,颜色的划分,状态机等等.....

枚举类型的创建方法:

2.使用

(1).switch语句

java 复制代码
public enum enumTest {
    RED,BLACK,BLUE,WHITE;
    public static void main(String[] args) {
        enumTest enumTest=BLACK;
        switch (enumTest){
            case RED :
                System.out.println("RED");
                break;
            case BLACK:
                System.out.println("BLACK");
                break;
            case BLUE:
                System.out.println("BLUE");
                break;
            case WHITE:
                System.out.println("WHITE");
            default:
                System.out.println("未匹配");
                break;
        }
    }
}

(2).常用方法

|-------------|------------------|
| 方法名称 | 描述 |
| values() | 以数组形式返回枚举类型的所有成员 |
| ordinal() | 获取枚举成员的索引位置 |
| valueOf() | 将普通字符串转换成枚举实例 |
| compareTo() | 比较两个枚举成员在定义时的顺序 |

①values
②ordinal
③valueOf
④.compareTo()

当我们查看compareTo的原码的时候,我们发现,其实是两个下标的相减

(3).枚举的构造方法是私有的

可以看到,当我对枚举的构造方法添加public访问修饰限定符时,程序报错了

即使我在构造方法前面添加private访问修饰限定符的时候,发现也是灰色的,说明该构造方法默认就是由private修饰的

3.枚举的优缺点

优点:

①.枚举常量更简单安全

②.枚举具有内置方法,代码更优雅

缺点:

不可继承,无法扩展

4.枚举和反射

我们想一下,前面我们可以反射私有的构造方法,那么枚举的构造方法也是私有的,我们是否可以反射枚举的构造方法?

首先,答案是不可以的。

下面通过代码来解析一下为什么不可以

当代码写到这的时候,我们运行起来,发现,程序报错了

报错的原因大致是,在enumdemo包下的enumTest类中,不存在一个参数为(String,int)的构造方法

但是我们的构造方法明明就在那里

我们回想一下,我们自己写的枚举类,就算没有显示继承Enum,但是其默认继承了Enum这个类

。也就是说我们自己写的enum类默认是继承于Enum这个父类的,当子类继承于父类之后,子类要帮助父类进行构造,而我们写的类,并没有帮助父类构造。我们看一下Enum的原码

这是父类的构造方法

枚举比较特殊,虽然我们自己写的构造方法传的是两个参数,但是其默认还添加了两个参数,默认的这两个参数就是父类的构造方法的两个参数。

也就是说,我们这里需要给出4个参数

当程序再运行起来的时候,我们发现

在16行的newInstance()方法处报了一个错误

当看newInstance()方法的原码的时候,发现

枚举在这里被过滤掉了,也就是说,我们不能通过反射获取枚举类的实例!

5.总结

①.枚举本身就是一个类,其构造方法默认是私有的,且都是默认继承于java.lang.Enum

②.枚举可以避免反射和序列化的问题

③.枚举的优点和缺点

(三).Lambda表达式

1.背景

Lambda表达式是Java SE 8 中的一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。Lambda表达式 ,基于数据中的λ演算得名,也可称为闭包(Closure)

2.Lambda表达式的语法

基本语法:(parameters)->expression或(parameters)->{statements;}

Lambda表达式由三部分组成:

①.parameters:类似方法中的形参列表,这里的参数是函数式接口里的参数。这里的参数类型可以明确的声明,也可以不声明,而由JVM隐含的推断。另外当只有一个推断类型时可以省略掉圆括号。

②.->:可以理解为"被用于"的意思

③.方法体:可以是表达式也可以是代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不返回,这里的代码块等同于方法的方法体。如果是表达式,也可以返回一个值或者什么都不返回

3.函数式接口

要想了解Lambda表达式,首先要了解什么是函数式接口,函数式接口的定义:一个接口有且只有一个抽象方法。

java 复制代码
@FunctionalInterface
interface IA{
    void test1();

    default void test2(){

    }
    public static void test3(){

    }
}

在IA接口中,虽然由多个方法,但是抽象方法只有一个test1()。此时我们就称IA接口为函数式接口

如果我们在接口上声明了**@FunctionalInterface**注解,那么编辑器就会按照函数式接口的定义来要求该接口,如果有两个抽象方法,程序编译就会报错。所以,从某种意义上来说,只要你保证你的接口只有一个抽象方法,可以不加这个注解。加上就会自动进行检测的。

4.Lambda表达式的基本使用

java 复制代码
//无返回值并且无参数
interface NoParameterNeReturn{
    void test();
}
//无返回值并且有一个参数
interface OneParameterNoReturn{
    void test(int a);
}
///无返回值并且有多个参数
interface MoreParameterNoReturn{
    void test(int a,int b);
}
//有返回值并且无参数
interface NoParameterReturn{
    int test();
}
//有返回值并且有一个参数
interface OneParameterReturn{
    int test(int a);
}
//有返回值并且有多个参数
interface MoreParameterReturn{
    int test(int a,int b);
}

我们通过上面这6个接口来具体看一下Lambda表达式的使用

语法精简

i.参数类型可以省略,如果需要省略,每个参数的类型都要省略

ii.参数的小括号里面只有一个参数,那么小括号可以省略

iii.如果方法体当中只有一句代码,那么大括号可以省略

iv.如果方法体中只有一条语句,且是return语句,那么大括号可以省略,且去掉return关键字

①.无返回值并且无参数

java 复制代码
        NoParameterNoReturn noParameterNoReturn1=()->{
            System.out.println("使用lambda表达式调用的无返回值并且无参数");
        };
        noParameterNoReturn1.test();

这一部分还可以再省略

java 复制代码
        NoParameterNoReturn noParameterNoReturn1=()-> System.out.println("使用lambda表达式调用的无返回值并且无参数");
        noParameterNoReturn1.test();

②.无返回值并且有一个参数

java 复制代码
        OneParameterNoReturn oneParameterNoReturn1=(a)->{
            System.out.println(a);
        };
        oneParameterNoReturn1.test(20);

同样,这部分也可以进行省略

java 复制代码
        OneParameterNoReturn oneParameterNoReturn1=a-> System.out.println(a);

        oneParameterNoReturn1.test(20);

③.无返回值并且有多个参数

java 复制代码
        MoreParameterNoReturn moreParameterNoReturn1=(int a,int b)->{
            System.out.println(a+b);
        };
        moreParameterNoReturn1.test(2,8);

同样,这部分也可以进行省略

java 复制代码
        MoreParameterNoReturn moreParameterNoReturn1=(a,b)-> System.out.println(a+b);
        
        moreParameterNoReturn1.test(2,8);

④.有返回值并且无参数

java 复制代码
        NoParameterReturn noParameterReturn1=()->{
            return 50;
        };
        System.out.println(noParameterReturn1.test());

同样这部分也可以进行省略

java 复制代码
        NoParameterReturn noParameterReturn1=()->50;
        System.out.println(noParameterReturn1.test());

⑤.有返回值并且有一个参数

java 复制代码
        OneParameterReturn oneParameterReturn1=(int a)->{
            return a;
        };
        System.out.println(oneParameterReturn1.test(10));

同样,这部分代码也可以进行省略

java 复制代码
        OneParameterReturn oneParameterReturn1=a->a;
        System.out.println(oneParameterReturn1.test(10));

⑥.有返回值并且有多个参数

java 复制代码
        MoreParameterReturn moreParameterReturn1=(int a,int b)->{
            return a+b;
        };
        System.out.println(moreParameterReturn.test(1, 2));

同样,这部分代码也可以省略

java 复制代码
        MoreParameterReturn moreParameterReturn1=( a, b)-> a+b;
        System.out.println(moreParameterReturn.test(1, 2));

5.变量捕获

①.匿名内部类的变量捕获

如果在匿名内部类中用到了变量a,则这个变量a在用之前和用之后包括用的过程中都是不能进行改变的

或者这个变量a为常量

这四种情况都会报错

②.Lambda表达式中的变量捕获

如果在Lambda表达式里用到了外面的变量a,那么这个变量a一旦赋值后,不管是在写Lambda表达式之前,之后,还是在Lambda表达式运行的时候,都不能再改动它的值。

同样上面三种情况也会报错

6.Lambda表达式在集合当中的应用

在创建优先级队列的时候,我们在实现大根堆的时候我们在 PriorityQueue 中传入了一个比较器

java 复制代码
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1.compareTo(o2);
            }
        });
    }

通过学习了Lambda表达式后,我们可以这样进行表示

java 复制代码
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue=new PriorityQueue<>((o1,o2)->{
            return o1.compareTo(o2);
        });
    }

为了能够让Lambda和Java的集合类集更好的一起使用,集合当中,也新增了部分接口,以便与Lambda表达式对接。

|------------|-------------------------------------------------------------------------------------------------------------------------------|
| 对应的接口 | 新增的方法 |
| Collection | removelf() spliterator() stream() parallelStream() forEach() |
| List | replaceAll() sort() |
| Map | getOrDefault() forEach() replaceAll() putlfAbsent() remove() replace() computelfAbsent() computelfPresent() compute() merge() |

①.Collection中的forEach()方法

打印元素

由于ArrayList本身是实现了是实现了Collection接口的,Collection里面有一个forEach()方法

通过上面的图片可以看到,forEach()方法里面参数类型为Consumer的接口类型,所以说,当我们使用forEach()方法的时候,我们需要传进去个Consumer的接口类型,调用里面的方法,把数组的元素放到accept里面,然后进行打印,当点开底层的accept方法,可以看到,为抽象方法,也就是说,想用这个方法,就要对其进行重写

java 复制代码
    public static void main(String[] args) {
        ArrayList<Integer> arrayList=new ArrayList<>();
        arrayList.add(1);
        arrayList.add(3);
        arrayList.add(7);
        arrayList.add(6);
        arrayList.add(2);
        arrayList.add(9);

        arrayList.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.print(integer+" ");
            }
        });
        System.out.println();
        System.out.println("===使用Lambda表达式===");
        arrayList.forEach((integer -> {
            System.out.print(integer+" ");
        }));
    }

同样也可以使用Lambda表达式解决该问题

②.List接口中的sort()方法

也可以先进行排序然后再进行打印,都是用的Lambda表达式

③.Map接口中的forEach()方法

7.总结

Lambda表达式的优点很明显,在代码层次上来说,使代码变得非常的简洁。缺点也很明显,代码不易读。

优点:

①.代码简洁,开发迅速

②.方便函数式编程

③.非常容易进行并行就算

④.Java引入Lambda,改善了集合操作

缺点:

①.代码可读性变差

②.在非并行计算中,很多计算未必有传统的for性能更高

③.不容易进行调试

(四).泛型进阶

泛型进阶主要介绍的是通配符

1.通配符

通过一个例子来看

java 复制代码
class Message<T>{
    public T message;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }
}
public class test {
    public static void main(String[] args) {
        Message<String> stringMessage=new Message<>();
        stringMessage.setMessage("hello world!!!");
        func(stringMessage);
    }

    private static void func(Message<String> message) {
        System.out.println(message.getMessage());
    }
}

上面的代码跑起来一点问题没有

但是,当我稍微修改一下,程序就会出现问题

当我定义一个泛型为Integer类型的Message的时候,我再去调用func方法就报错了,原因是无法进行类型转换,因为func方法只接受泛型为String类型的Message

那么我们如何解决?

可以使用通配符

站在func的角度,func也不知道将来会接收什么类型的参数,所以就写一个 "?",这个 "?"就叫通配符。

2.通配符的上界

java 复制代码
<? extends 上界>
<? extends Number>  //可以传入的实参类型为Number或者Number的子类

通过一个例子来看

java 复制代码
class Message<T>{
    public T message;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }
}
class Fruit{

}
class Apple extends Fruit{

}
class Banana extends Fruit{

}
public class test {

    public static void main(String[] args) {
        Message<Apple> message1=new Message<>();
        Message<Banana> message2=new Message<>();
        func1(message1);
        func1(message2);
    }

    private static void func1(Message<? extends Fruit> message) {
        //父类引用 引用子类对象
        Fruit fruit=message.getMessage();
    }
}

3.通配符的下界

java 复制代码
<? super 下界>
<? super Integer> //表示可以传入的实参的类型是Integer或者Integer的父亲类型

通过例子来看

java 复制代码
    public static void main(String[] args) {
        Message<Fruit> message=new Message<>();
        func2(message);
    }

    //通配符的下界,接收的要么是Fruit,要么是Fruit的父类
    private static void func2(Message<? super Fruit> message) {
        //这里可以的原因是,Apple至少是一个Fruit,虽然接收的时候是Fruit或Fruit的父类
        //但是放的时候可以是Fruit的子类
        message.setMessage(new Apple());

        //向下转型
        Fruit fruit=(Fruit) message.getMessage();
    }
相关推荐
Zsy_0510032 小时前
【C++】类和对象(二)
开发语言·c++
Duang007_2 小时前
【万字学习总结】API设计与接口开发实战指南
开发语言·javascript·人工智能·python·学习
小北方城市网2 小时前
JVM 调优实战指南:从问题排查到参数优化
java·spring boot·python·rabbitmq·java-rabbitmq·数据库架构
一叶星殇2 小时前
C# .NET 如何解决跨域(CORS)
开发语言·前端·c#·.net
Elieal2 小时前
Java项目密码加密实现详解
java·开发语言
shhpeng2 小时前
go mod vendor命令详解
开发语言·后端·golang
Java程序员威哥2 小时前
用Java玩转机器学习:协同过滤算法实战(比Python快3倍的工程实现)
java·开发语言·后端·python·算法·spring·机器学习
GeekyGuru2 小时前
C++跨平台开发的核心挑战与应对策略
开发语言·c++
牧小七2 小时前
java StampedLock 的使用
java