Java基础学习(三)

 Von's Blog     2020-04-13   13754 words    & views

内部类

内部类顾名思义,就是一个类当中包含另一个类,包含成员内部类和局部内部类。

成员内部类

成员内部类的定义格式为:

修饰符 class 外部类名称{
  修饰符 class 内部类名称{
    
  }
} 

内部类可以任意调用外部类的属性和方法,而外部类如果要使用内部类的属性和方法,必须要借助于内部类对象。

假如在一个类C1中编写了一个内部类C2,在编译的时候,会生成两个文件: C1.classC1$C2.class ,我们可以把他们看作两个无关的类,在反射中通过 Class.forName("C1$C2")即可加载这个内部类。

成员内部类的使用

对于成员内部类的使用有两种方式:

  • 间接方式:在外部类方法中使用内部类,main方法只调用外部类的方法。

    public class Car {
        public class Engine{
            public void UseEngine(){
                System.out.println("Using Engine!");
            }
        }
      
        public void use_engine(){
            new Engine().UseEngine();
        }
    }
    

    上面的Car类中定义了一个Engine内部类,并且在外部类Car中直接定义了一个方法use_engine()使用了内部类对象的方法。这样我们在main方法中使用时就能通过直接使用use_engine()来达到间接使用内部类的效果了:new Car().use_engine();

  • 直接方式:直接创建内部类对象的定义格式:外部类名称.内部类名称 对象名 = new 外部类名称(参数列表).new 内部类名称(参数列表)

    也即其实还是先创建了一个外部类对象。对于上面的例子,直接创建内部类对象并使用的调用如下:

    Car.Engine engine = new Car().new Engine();
    engine.UseEngine();
    

成员内部类的同名变量访问

直接来看例子:

public class Out {
    int num = 1;
    public class Inner{
        int num = 2;
        public void test(){
            int num = 3;
            System.out.println(num);    //输出3
            System.out.println(this.num);    //输出2
            System.out.println(Out.this.num);    //输出1
        }
    }
}

其实就是内部类如果想使用外部类的成员变量,调用规则为外部类名称.this.变量名称

局部内部类

一个类如果是定义在方法内部的,那么这就是一个局部内部类。其定义格式为:

修饰符 class 外部类名称{
  修饰符 返回值类型 外部类方法名称(参数列表){
    class 局部内部类名称{
      
    }
  }
}

只有当前所属的方法才能使用该类,方法外面不允许使用该类,而且需要注意的是:局部内部类前面不允许有任何修饰符。局部内部类的使用方法如例子所示:

public class Out {
    public void test(){
        class Inner{
            int num = 1;
        }
        Inner in = new Inner();
        System.out.println(in.num);
    }
}

在test方法中定义了一个局部内部类并在方法中进行实例化并使用,而由于局部内部类只能由方法本身使用,所以对外也只能通过调用该方法实现对局部内部类的利用。

Out out = new Out();
out.test();

局部内部类的final问题

局部内部类如果想访问所属方法中的局部变量,需要该变量是有效final的,众所周知,变量如果用final修饰即表明其不可被更改。所谓有效final即分两种情况:

1、直接使用final修饰该变量使其值不可被更改

2、虽然没有使用final修饰该变量但在方法中该变量值自从被定义后就不曾修改。

所以以下这两种情况代码都是正确的:

public class Out {
    public void test(){
        final int num =1; 
        class Inner{
            int num1 = num+1;
        }
        Inner in = new Inner();
        System.out.println(in.num1);
    }
}
public class Out {
    public void test(){
        int num =1;
        class Inner{
            int num1 = num+1;
        }
        Inner in = new Inner();
        System.out.println(in.num1);
    }
}

而以下这种写法是不被允许的,因为变量num并不是有效final的:

public class Out {
    public void test(){
        int num =1;
        num =2;
        class Inner{
            int num1 = num+1;
        }
        Inner in = new Inner();
        System.out.println(in.num1);
    }
}

至于为什么有这个问题其实是生命周期的原因,因为局部变量位于栈内存中,在方法运行结束之后会立刻出栈,局部变量会立刻消失。而new出来的对象会在堆中持续存在,直至垃圾回收消失。

匿名内部类

如果接口的实现类或者父类的子类的实例只需要被创建一次,那么这种情况下就可以省略掉该类的定义,改用匿名内部类。以接口为例,其定义格式如下:

接口名称 对象名 = new 接口名称(){
  //复写所有抽象方法
};

来看例子:

public interface Myinter {
    void test();
}

假如我们想在代码中为其创建一个匿名内部类并调用的话,代码为:

Myinter obj = new Myinter() {
  @Override
  public void test() {
    System.out.println("This is a test method.");
  }
};
obj.test();

匿名内部类和匿名对象很类似但又有所不同,匿名内部类省略的是实现类的名称,但是没有省去实现的对象名称(obj),假如想同时使用匿名内部类和匿名对象的话,可将上面代码改造为:

new Myinter() {
  @Override
  public void test() {
    System.out.println("This is a test method.");
  }
}.test();

当然这样该对象就只能调用一个方法并且只能被使用一次了。

泛型

泛型其实就是一种未知的数据类型,在实际创建数据类型的时候再将数据类型赋予进行确定。

泛型解决的问题

对于以下的代码:

ArrayList list = new ArrayList();
list.add(1);
list.add("sushuo");
for (int i = 0; i < list.size(); i++) {
  System.out.println((String) list.get(i));
}

在编译的时候并不会产生问题(ArrayList本来就可以存储任意类型数据),但是运行时则会抛出异常(int没办法被强制类型转换为String)

这种只有运行时才会出现的问题不是我们希望看到的,我们希望将发现问题的时间提前至编译时,这样我们在IDEA中写代码的时候就能及时更正了。泛型就是为了解决这一问题而生的,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

而采用泛型后定义就变成了:

ArrayList<String> list = new ArrayList<>();

此时在编译阶段就已经无法执行list.add(1)这种操作了,这就是泛型存在的意义。

泛型的生命周期

泛型只在只在编译阶段有效,也就是说泛型是提供给Javac编译器看的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,在正确检验泛型结果后,会将泛型的相关信息抹去。

ArrayList<String> c1 = new ArrayList<>();
ArrayList<Integer> c2 = new ArrayList<>();
System.out.println(c1.getClass() == c2.getClass());   //输出为true

因此假如采用反射调用方法就能绕过编译器的检测(因为反射执行操作的时间已经是在编译以后了,编译期生成的class对象),如下便往一个原本只能存储整数类型的ArrayList c2中添加了一个字符串Sushuo。

ArrayList<String> c1 = new ArrayList<>();
ArrayList<Integer> c2 = new ArrayList<>();
c1.getClass().getDeclaredMethod("add", Object.class).invoke(c2,"Sushuo");
System.out.println(c2.get(0));    //输出Sushuo

但我所不理解的是,为什么反向的操作试图往c1添加一个Int类型代码会抛出异常,如果有师傅看到这个问题欢迎留言解答呀。

ArrayList<String> c1 = new ArrayList<>();
ArrayList<Integer> c2 = new ArrayList<>();
c1.getClass().getDeclaredMethod("add", Object.class).invoke(c1,111);
System.out.println(c1.get(0));

//异常信息: Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')

含有泛型的类

泛型类的定义方式如下:

修饰符 class 类名<泛型标识> {
  
}

其中的泛型标识可以是任意字符,一般常用T、E、K、V等表示,泛型标识即相当于代替了数据类型的存在,可以在接下来的全局中替代使用。泛型类在实例化类的时候指明泛型的具体类型,具体使用如下:

public class Generic<E> {
    private E name;

    public E getName() {
        return name;
    }

    public void setName(E name) {
        this.name = name;
    }
}

如上定义了一个类,可以看到其实就是将我们平时常用的String等数据类型替换成E,当实例化类时确定具体的类型:

Generic<String> gene1 = new Generic<>();
gene1.setName("XR");
System.out.println(gene1.getName());
Generic<Integer> gene2 = new Generic<>();
gene2.setName(111);
System.out.println(gene2.getName());

其实定义的泛型类,并不一定要传入泛型类型实参,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。比如此种写法也是允许的:Generic gene1 = new Generic();,则默认为传入的E为Object.

含有泛型的方法

泛型方法,是在调用方法的时候指明泛型的具体类型。定义格式如下:

修饰符 <泛型标识> 返回值类型 方法名(参数){
  
}

在上面的基础上继续改造:

public class Generic<E> {
    private E name;

    public E getName() {
        return name;
    }

    public void setName(E name) {
        this.name = name;
    }

    public <T> void show(T id){
        System.out.println(id);
    }
}
Generic<String> gene1 = new Generic<>();
gene1.show("111");

这里面一个很重要的点就是getName()setName()虽然使用了泛型,但却不是泛型方法,只有最后的show()方法才是泛型方法,可以说最重要的区别就在于有无中间的<T>,而getName()setName()能使用泛型不过是因为泛型类提供的泛型而已,其本身并没有声明新的泛型。

此外静态方法没办法使用泛型类中定义的泛型,即这种情况是不允许的:

public class Generic<E> {
    public static void show_id(E id){

    }
}

只能通过将静态方法也定义成泛型方法来解决:

public class Generic<E> {
    public static <E> void show_id(E id){

    }
}

含有泛型的接口

接口中也能使用泛型,定义格式为:

修饰符 interface 接口名 <泛型标识> {
  
}
public interface Myinter<E> {
    void test(E name);
}

泛型接口有两种使用方式:

  • 定义接口的实现类,并指定接口的泛型。

    public class Test implements Myinter<String>{
        @Override
        public void test(String name) {
            System.out.println(name);
        }
    }
    
  • 接口使用什么泛型,实现类就使用什么泛型(相当于定义了一个泛型类),在实例化对象的时候再确定泛型。

    public class Test<I> implements Myinter<I>{
        @Override
        public void test(I name) {
            System.out.println(name);
        }
    }
    

代理

Java中的代理

我们生活中使用代理可能是因为我们不能直接访问某个网站或服务,但在Java中的代理我们主要强调的是代理的中间人属性。即通过代理作为中间人来给我们要使用的对象动态的添加功能或进行过滤,如果我们直接调用A对象的a方法的话,那就只能运行a方法,但是如果我们给A对象添加一个代理 ,然后我们在代理类中就可以在a方法前面添加b方法,在a方法后面添加c方法,然后我们通过代理类的对象D去调用a方法的时候就能同时运行b和c方法。

静态代理

假设我们现在有一个车辆充电接口:

public interface Charging {
    void charge();
}

我们通过一个类来实现该接口(在代理中实现该接口的类称为委托类):

public class TeslaCharge implements Charging{
    @Override
    public void charge() {
        System.out.println("Use Tesla Charging");
    }
}

假如我们还想代理来实现充电前输出当前电压的功能,我们需要实现一个代理类:

public class TeslaProxy implements Charging{
    private Charging charging;
    public TeslaProxy(Charging charging){
        this.charging = charging;
    }
    @Override
    public void charge() {
        System.out.println("Voltage is 300V!");
        this.charging.charge();
        System.out.println("Charging finish!");
    }
}

代理类的思路就是在构造方法中传入一个委托类的对象,然后由代理类重写委托类中需要增加功能的方法(需要在当中调用委托类的响应方法),此后通过调用代理类的该方法便能在不修改委托类的情况下新增相应功能。

TeslaProxy tes = new TeslaProxy(new TeslaCharge());
tes.charge();

输出为:

Voltage is 300V!
Use Tesla Charging
Charging finish!

静态代理的缺点

静态代理的最大问题就是每次要代理一个委托类都需要新定义一个代理类。 第二个问题就是假如在接口中定义了抽象方法,接口中新增或删减抽象方法后委托类和代理类便都需进行相应更改,代码维护困难。

动态代理

针对静态代理的缺点,动态代理应运而生。动态代理不需要去实现所代理的类的接口,使用起来更加方便。动态代理的使用流程如下:

  1. 定义一个公共接口和委托类,这里我们仍然使用上面的Charging接口和TeslaCharge类。

    public interface Charging {
        void charge();
    }
    
    public class TeslaCharge implements Charging{
        @Override
        public void charge() {
            System.out.println("Use Tesla Charging");
        }
    }
    
  2. 定义一个实现InvocationHandler接口(java.lang.reflect.InvocationHandler)的类(找了一圈似乎也没发现这个类的官方名字,我就称之为中间类吧)并覆写其中的invoke方法

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
       
    public class TeslaProxy implements InvocationHandler {
        private Object object;
        public TeslaProxy(Object object){
            this.object = object;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("Voltage is 300V!");
            Object result = method.invoke(object,args);
            System.out.println("Charging finish!");
            return result;
        }
    }
    
  3. 利用Proxy类(java.lang.reflect.Proxy)动态生成代理类,关于Proxy类,在动态代理中我们最常用到的它的一个静态方法

    static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
    

    该方法返回一个代理对象,传入的三个参数分别为:

    • ClassLoader loader:定义了生成的这个代理类的加载器
    • Class<?>[] interfaces:声明你这个代理类需要实现哪些接口
    • InvocationHandler h:一个中间类的实例

    newProxyInstance方法的方便之处在于直接创建了代理对象(略去了动态创建代理类的代码),而假如我们想把动态生成的代理类的字节码保存下来。如果在Java8及之前的版本,在调用newProxyInstance之前设置:

    System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")
    

    如果是Java8之后的版本,则设置:

    System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
    或者
    System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
    

    完整利用代码为:

    // Java11
    TeslaCharge teslaCharge = new TeslaCharge();
    TeslaProxy teslaProxy = new TeslaProxy(teslaCharge);
    System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
    Charging charging = (Charging) Proxy.newProxyInstance(TeslaCharge.class.getClassLoader(),TeslaCharge.class.getInterfaces(),teslaProxy);
    charging.charge();
    

    输出为:

    Voltage is 300V!
    Use Tesla Charging
    Charging finish!
    

    动态生成的动态代理类为:

    package com.sun.proxy;
       
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
       
    public final class $Proxy0 extends Proxy implements Charging {
        private static Method m1;
        private static Method m2;
        private static Method m3;
        private static Method m0;
       
        public $Proxy0(InvocationHandler var1) throws  {
            super(var1);
        }
       
        public final boolean equals(Object var1) throws  {
            try {
                return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
                throw var3;
            } catch (Throwable var4) {
                throw new UndeclaredThrowableException(var4);
            }
        }
       
        public final String toString() throws  {
            try {
                return (String)super.h.invoke(this, m2, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
       
        public final void charge() throws  {
            try {
                super.h.invoke(this, m3, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
       
        public final int hashCode() throws  {
            try {
                return (Integer)super.h.invoke(this, m0, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
       
        static {
            try {
                m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
                m2 = Class.forName("java.lang.Object").getMethod("toString");
                m3 = Class.forName("Charging").getMethod("charge");
                m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch (NoSuchMethodException var2) {
                throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
                throw new NoClassDefFoundError(var3.getMessage());
            }
        }
    }
    

    这个动态代理类有几个值得注意的点:

    • 命名方式为 $ProxyN,其中N会慢慢增加,一开始是 $Proxy1,接下来是$Proxy2
    • 继承Proxy类,并实现了在Proxy.newProxyInstance()中提供的接口数组
    • 使用final进行修饰,即不能有子类

    回过头再来看这段实现代码:

    // Java11
    TeslaCharge teslaCharge = new TeslaCharge();
    TeslaProxy teslaProxy = new TeslaProxy(teslaCharge);
    System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
    Charging charging = (Charging) Proxy.newProxyInstance(TeslaCharge.class.getClassLoader(),TeslaCharge.class.getInterfaces(),teslaProxy);
    charging.charge();
    

    这里采用TeslaCharge.class.getClassLoader()TeslaCharge.class.getInterfaces()分别获得类加载器和需实现的接口。由于newProxyInstance返回的是一个Object对象,如果只使用Object对象的话功能是相当受限的,由于其实现了Charging这个接口,因此我们可以对其做一次向下转型转成Charging类型的变量以使其能调用相关方法。

对动态代理的思考

我觉得动态代理最大的优点就是把逻辑层代码独立出来。最终调用中,向newProxyInstance方法中传入的三个参数,两个是委托类TeslaCharge本身具有的属性;第三个参数是中间类TeslaProxy的实例对象,而中间类的编写完全与其他接口与类实例无关。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TeslaProxy implements InvocationHandler {
    private Object object;
    public TeslaProxy(Object object){
        this.object = object;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Voltage is 300V!");
        Object result = method.invoke(object,args);
        System.out.println("Charging finish!");
        return result;
    }
}

只要是方法调用前后需要分别输出”Voltage is 300V!”还有”Charging finish!”的其他任意接口和委托类,都能使用这个中间类而无需我们自己去定义代理类。

当然由于Java的单继承特性,动态代理出来的代理类已经继承了Proxy类也就无法继承其他类了。这也即JDK的动态代理的缺点:不能代理一个类,只能代理接口,而cglib可以做到基于类的动态代理。

参考文章

JAVA 泛型、动态代理技术要点梳理

Java 静态代理&动态代理学习

JAVA安全基础(三)– java动态代理机制

Java静态代理&动态代理