• JAVA核心技术笔记总结--第6章 抽象类、接口、内部类和Lambda表达式


    6.1 抽象类

    抽象类是指定义时有 abstract 修饰的类,例如:

    public abstract class Person{
      ...
        public abstract String getDescription();//注意末尾没有花括号,而有分号
    }
    

    在定义中有abstract 修饰符的方法是抽象方法。抽象类中可以包含实例变量和实例方法,甚至可以没有抽象方法,但是有抽象方法的类一定要定义为抽象类。

    抽象方法充当着占位的角色,它们的具体实现在子类中。抽象方法不能有方法体,即没有花括号,但必须有分号,方法定义变成了方法声明。扩展抽象类可以有两种选择:

    1. 若子类只实现了父类中部分抽象方法,此时子类也为抽象类
    2. 若子类中实现了父类中所有抽象方法,此时子类为抽象类的实现类,子类是一个普通类,不再是抽象类。

    抽象类不能实例化(因为抽象方法没有具体实现,即使抽象类中不包含抽象方法),可以定义抽象类的变量,但它只能引用其实现类的对象。

    抽象类可以包含实例变量、实例方法、类变量、静态方法、构造器、静态初始化块、普通初始化块、内部类。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。在抽象类中,实例方法可以调用抽象方法。以下面的代码为例,因为子类CarSpeedMeter实现了抽象方法getRadius,相当于子类覆盖了该方法,从而具有多态性,子类对象调用getSpeed时,getSpeed中调用的getRadius方法是子类中的getRadius

    public abstract class SpeedMeter{
      private double turnRate;
      public abstract double getRadius(); 
      public double getSpeed(){
        return Math.PI * 2 * getRadius();
      }
    }
    public class CarSpeedMeter extends SpeedMeter{
      public double getRadius(){
        ...
      }
    }
    

    6.2 接口

    6.2.1 Java 8中接口的定义

    定义接口时不再使用class关键字,而使用interface关键字。接口定义的基本语法如下:

    [修饰符] interface 接口名 extends 父接口1,父接口2,...{
      零个到多个静态常量定义。。。
      零个到多个抽象方法定义...
      零个到多个默认方法定义...//仅在java8中允许
      零个到多个内部类、接口、枚举定义
    }
    

    由于接口里定义的是多个类共同的公共行为规范,因此接口里所有成员都是默认public访问权限。接口里不包含成员变量,构造器和初始化块。接口里只能包含静态常量、抽象实例方法、类方法、默认方法、内部类、内部接口、内部枚举。

    静态常量可以省略public static final,系统默认添加。且静态常量只能在定义时初始化。

    对于抽象方法,系统默认添加public abstract修饰,因此,抽象方法不能有方法体。从而,实现类覆盖这些抽象方法时,访问权限必须是public

    默认方法需要加default修饰符,但不能有static修饰符,否则,和类方法没有区别。系统默认添加public修饰。以鼠标监听接口MouseListener来说明默认方法存在的目的,MouseListener包含5个接口:

    interface MouseListener{
      void mouseClicked(MouseEvent event);
      void mousePressed(MouseEvent event);
      void mouseReleased(MouseEvent event);
      void mouseEntered(MouseEvent event);
    }
    

    大多数情况下,只需要关心前两个接口,在Java8中可以把所有方法声明为默认方法,这些方法什么都不做:

    interface MouseListener{
      default void mouseClicked(MouseEvent event){}
      default void mousePressed(MouseEvent event){}
      default void mouseReleased(MouseEvent event){}
      default void mouseEntered(MouseEvent event){}
    }
    

    从而,实现此接口的程序员只需要重写他们真正关心的方法。默认方法可以调用其他的默认或抽象方法。

    静态方法是java8增加的功能,在此之前,Java多会给接口实现一个伴随类中,并将静态方法放在伴随类中。当允许在接口中定义静态方法时,这些伴随类将不再需要。

    接口里的内部类、内部接口、内部枚举,系统默认添加public static修饰,因为不能创建接口实例。

    定义接口的示例如下:

    public interface Output{
      int MAX_CACHE_LINE = 50;
      void out();
      static String staticTest(){
        return "类方法";
      }
      default void test(){
        System.out.println("默认方法");
      }
    }
    
    6.2.2 接口的继承

    接口支持多继承,一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。子接口扩展父接口时,将会获得父接口所有的默认方法和抽象方法。

    一个接口继承多个接口时,多个接口排在extends关键字之后,多个父接口之间以英文逗号隔开。示例如下:

    interface A{
      int PRO_A = 5;
      void testA();
    }
    interface B{
      int PRO_B = 6;
      void testB();
    }
    interface C extends A,B{
      int PRO_C = 7;
      void testC();
    }
    
    6.2.3 使用接口

    接口不能创建实例,但接口可以声明变量。但接口声明的变量必须引用到其实现类的对象。除此之外,接口的主要功能是被实现类实现。接口的主要用途归纳如下:

    • 定义变量,也可用于强制类型转换。
    • 调用接口中定义的静态常量和静态方法。
    • 被其他类实现。

    一个类可以实现一个或多个接口,继承使用extends,实现则使用implements。允许一个类实现多个接口,可以弥补java单继承的不足,同时避免多继承的复杂性和低效性。类实现接口的语法格式如下:

    [修饰符] class 类名 extends 父类 implements 接口1,接口2...{
      类体部分
    }
    

    实现接口与继承父类相似,一样可以获得所实现接口里定义的静态常量、方法(抽象方法和默认方法)。

    让类实现接口需要类定义后增加implements部分,当需要实现多个接口时,多个接口之间以英文逗号隔开,一个类可以继承父类,并同时实现多个接口,implements部分必须方法extends部分之后。

    一个类实现类一个或多个接口之后,这个类必须完全实现这些接口里定义的全部抽象方法(即重写这些抽象方法,包括接口从父接口继承得到的抽象方法);否则,该类必须定义为抽象类。

    接口不能显式继承任何类,但所有接口类型的变量都可以直接赋给Object类型的变量,因为编译器知道接口类型变量的运行时类型必定是实现类对象,而任何Java对象都必须是Object或其子类的实例。

    6.2.4 接口和抽象类

    接口和抽象类很像,他们的共同点有:

    • 接口和抽象类都不能被实例化。
    • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的子类都必须实现这些方法。

    但接口和抽象类的差别很大,主要体现在两者的设计目的上:

    • 接口定义了一组规范,要求实现者必须提供哪些服务。当在程序中使用接口时,接口是多个模块间的耦合标准。
    • 抽象类作为多个子类的共同父类,所体现的是一种模板式设计。

    接口和抽象类在用法上的区别如下:

    • 接口中所有成员的访问权限默认为public,而抽象类中各种访问权限都可以。
    • 接口里只能包含抽象方法、默认方法和类方法。不能为普通方法提供实现;抽象类可以包含实例方法。
    • 接口里只能定义静态常量;抽象类可以定义成员变量、类变量。
    • 接口不能包含构造器和初始化块;抽象类中可以包含构造器和初始化块。
    • 一个类最多只能有一个直接父类,但一个接口可以继承多个直接父接口。
    6.2.5 解决默认方法冲突
    • 当一个类继承了父类,并实现了接口,若接口中的默认方法和父类中的方法,名称相同且形参列表相同时,父类的方法会覆盖接口的默认方法。这样做的目的是与java8之前的代码兼容。
    • 当一个类继承了父类,并实现了接口,若接口中的抽象方法和父类中的方法,名称相同且形参列表相同时,那么子类相当于已经有了接口抽象方法的默认实现。
    • 当一个类实现了多个接口,若其中两个及以上的接口有同名且形参列表相同的默认方法,或者一个接口的默认方法和另一个接口的抽象方法满足覆盖条件时,编译器会报错,解决方法是:在子类中覆盖此方法,方法体中可以选择执行哪个接口的默认方法。语法格式为:"接口名.super.方法名"
    6.2.6 接口与回调

    回调指对象调用某个方法时,此方法需要的实参是一个接口,而这个接口和对象有关,从而,对象调用方法后,方法反过来调用这个接口。

    6.2.7 Java中常用的接口
    - Comparable<T>
      接口中定义了:
      public int comPareTo(T other);
    - Comparator<T>(比较器)
      接口中定义了
      public int compare(T first, T second);
    - Clonable<T>
      public T clone();
    

    深克隆与浅克隆。

    Object类实现了基本的clone()方法,但由于不了解对象的域,所以只能逐个域地进行拷贝,对于引用类型的域,拷贝域就会得到相同子对象的另一个引用,从而,原对象和克隆得到的对象仍会共享一些信息,此为浅克隆。

    深克隆的做法是:对于基本数据类型,直接拷贝,对于引用类型的域,递归拷贝引用对象的每个域。使得原对象和克隆得到的对象不再共享任何信息(对于不可变类,可直接复制值,只有可变类,才需要这么处理)。调用此类型的clone()进行拷贝。

    自定义克隆方法时,需要确定:

    1. 默认的Object类的克隆方法是否满足要求。
    2. 是否可以在可变的子对象上调用克隆方法来修补默认的克隆方法。
    3. 是否不该使用克隆方法。

    如果选择第1项或第2项,类必须:

    1. 实现Cloneable接口
    2. 重新定义clone(),并指定public访问修饰符。

    由于Object类中的clone()声明为protected,所以子类只能调用此方法来克隆自己的对象,必须重新定义clone()public才能允许所有方法克隆对象。

    深克隆示例如下:

    class Employee implements Cloneable{
      ...
        public Employee clone() throws CloneNotSupportedException{
    		Employee cloned = (Employee) super.clone();
      		cloned.hireDay = (Date) hireDay.clone();
        	return cloned;
      }
    }
    

    所有数组类型都有一个publicclone方法。

    6.3 内部类

    在一个类内部定义的另一个类称为内部类,此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类称为局部内部类和匿名内部类)。包含内部类的类称为外部类。内部类的主要作用有:

    • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
    • 因为内部类被当成外部类成员,而同一个类的成员之间可以相互访问,所以内部类方法可以直接访问外部类的私有数据。但外部类要访问内部类的实现细节,必须先定义内部类对象。
    • 匿名内部类适合用于创建那些仅需要使用一次的抽象类或接口的实现类对象。

    定义内部类与定义外部类的语法大致相同,内部类除了需要定义在其他类里面之外,还存在如下两点区别:

    • 内部类比外部类可以多使用三个修饰符:private、protected、static
    • 非静态内部类不能有静态成员。
    6.3.1 成员内部类
    6.3.1.1 非静态内部类

    ​ 多数情况下,内部类作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类不是类成员。

    成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。

    因为内部类是作为其外部类的成员,所以可以使用任意访问控制符如private,protected 和 public 等修饰。编译器生成的成员内部类的class文件格式为:OuterClass$InnerClass.class。

    在非静态内部类里可以直接访问外部类的private成员。这是因为在非静态内部类对象里,保存了一个它所寄生的外部类对象的引用,同时编译器会在外部类中添加对相应私有域的访问器和更改器(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在外部类实例里)。

    当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用改变了;如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量,如果依然不存在,系统将出现编译错误,提示找不到该变量。

    因此,如果外部类成员变量、内部类成员变量与内部类方法的局部变量同名,则可通过使用外部类名.this,this作为限定区分。如下程序所示:

    public class TestVar{
      	private String prop = "外部类的实例变量";
        private class InClass{
    		private String prop = "内部类的实例变量";
            public void info(){
    			String prop = "局部变量";
                System.out.println("输出的是外部实例变量" + TestVar.this.prop);
                System.out.println("输出的是内部类的实例变量" + this.prop);
                System.out.println("输出的是局部变量" + prop);
            }        
        }
      	public void test(){
          Inclass in = new InClass();
          in.info();
        }
      public static void main(String[] args){
        new TestVar.test();
      }
    }
    

    非静态内部类的成员可以访问外部类的private成员,但反过来不成立。因为外部类对象存在时,非静态内部类对象不一定存在。如果外部类需要访问非静态内部类的成员,必须显式地创建内部类对象来访问其实例成员,外部类方法可以访问内部类的私有成员。(和普通的一个类的方法访问另一个类的方法存在区别,外部类访问内部类的实例成员,不需要通过内部类的公有方法来访问,这还是因为,大家都是外部类的成员,成员之间可以相互访问)。如下所示:

    public class Outer{
      private int outprop = 9;
      class Inner{
        private int inprop = 5;
        public void accessOuterprop{
          System.out.println("外部类的outprop值:" + outprop);
        }
      }
      public void accessInnerProp(){
        //System.out.println("内部类的inprop值" + inprop);
        System.out.println("内部类的inprop值" + new().inprop);
      }
      public static void main(String[] args){
        Outer out = new Outer();//注释2  
        out.accessInnerProp();
      }
    }
    

    第一处注释试图在外面类方法里访问非静态内部类的实例变量,将引起编译错误。外部类不允许访问非静态内部类的实例成员的原因是:上面main方法的第二处注释代码只创建了一个外部类对象,并调用外部类对象的accessInnerProp方法。此时非静态内部类对象根本不存在,如果允许accessInnerProp方法访问非静态内部类对象,将引起错误。

    非静态内部类对象和外部类对象的关系

    如果存在一个非静态内部类对象,则一定存在一个被它寄生的外部类对象。但当外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。所以外部类对象访问非静态内部类对象时,可能非静态内部类对象根本不存在,而非静态内部类对象访问外部类对象时,外部类对象一定存在。

    根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,尤其是不能使用非静态内部类创建实例等(原因是:使用非静态内部类时,外部类的对象并不存在,此时,非静态内部类对象无处寄生):

    public class StaticTest{
      	private class In{}  
      	piblic static void main(String[] args){
          //无法访问非静态成员 In类
          new In();
        }
    }
    

    java不允许在非静态内部类里定义静态成员、静态方法、静态初始化块。否则可以通过OutClass.InClass的方法来调用,此时,外部类对象并未创建。

    非静态内部类里不能有静态初始化块,但可以有普通初始化块,非静态内部类的普通初始化块的作用与外部类初始化块的作用相同。

    6.3.1.2 静态内部类

    如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象,相当于外部类的类成员。因此使用static修饰的内部类被称为静态内部类。

    static关键字的作用是把类的成员变成类相关,而不是实例相关,即static修饰的成员属于整个类,而不属于单个对象。对象类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。

    静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类的实例成员,只能访问外部类的静态成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类中的静态成员。原因是:静态内部类不需要寄生在外部类实例中,静态内部类的实例创建时,外部类的实例不一定存在。

    因为静态内部类是外部类的一个静态成员,因此外部类的所有方法,所有初始化块中可以使用静态内部类来定义变量、创建对象等。

    外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者访问静态内部类的静态成员,也可以使用静态内部类的对象访问静态内部类的实例成员。

    public lass AccessStaticInnerClass{
    	static class StaticInnerClass{
    		private static int prop1 = 5;
    		private int prop2 = 9;
    	}
    	public void accessInnerProp(){
        	System.out.println(StaticIneerClass.prop1);
        	System.out.println(new StaticIneerClass().prop2);
        }
    }
    
    6.3.2 使用内部类
    6.3.2.1 在外部类里面使用非静态内部类

    在外部类里面可以调用内部类对象的私有成员,但是需要先创建内部类对象。

    6.3.2.2 在外部类的实例方法中使用内部类
    • 定义内部类变量

      内部类名 变量名

    • 创建内部类对象

      new 内部类名(实参列表)

    • 访问内部类成员

      内部类对象名.成员名

    6.3.2.3 在外部类的静态方法中使用内部类
    • 定义内部类变量

      内部类名 变量名

    • 创建内部类对象

      在外部类静态方法中创建内部类实例时,需要先有外部类实例。然后:外部类实例名.new 内部类名(实参列表)

    • 访问内部类成员

      内部类对象名.成员名

    6.3.2.4 在外部类外面使用非静态内部类

    如果希望在外部类外面使用内部类,则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类里面使用,内部类的访问权限如下:

    • 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类访问
    • 使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类访问
    • 使用public修饰的内部类,可以在任何地方被访问。

    在外部类外访问内部类的格式如下:

    • 定义内部类变量

      外部类名.内部类名 变量名

    • 创建内部类对象

      在外部类外面创建内部类实例时,需要先有外部类实例。然后:外部类实例名.new 内部类名(实参列表)//此处不需要再写外部类.内部类。定义变量需要这么做是保证类的唯一性。

    • 访问内部类成员

      内部类对象名.成员名。此时只能访问内部类的公有成员

    6.3.2.5 在外部类里面使用静态内部类

    由于静态内部类是外部类相关的。所以内部类对象无需寄生于外部类对象。从而在外部类的静态方法和非静态方法中使用静态内部类格式相同,均为:

    • 定义内部类变量

      内部类名 变量名

    • 创建内部类对象

      new 内部类名(实参列表)

    • 访问内部类成员

      内部类对象名.成员名

    6.3.2.6 在外部类外使用静态内部类

    在外部类外使用静态内部类的格式如下:

    • 定义内部类变量

      外部类名.内部类名 变量名

    • 创建内部类对象

      new 外部类名.内部类名(实参列表)

    • 访问内部类成员

      内部类对象名.成员名。此时只能访问内部类的公有成员

    6.3.3 定义内部类的子类

    内部类的子类不一定是内部类,可以是一个外部类。

    6.3.3.1 定义非静态内部类的子类

    当创建一个非静态内部类的子类时,子类构造器总会调用父类的构造器,而调用非静态内部类的构造器时,必须存在一个外部类对象。因此在创建非静态内部类的子类时,必须给子类构造器传一个外部类对象作为参数。所以定义非静态内部类子类的格式为:

    class 子类名 extends 外部类名.内部类名{
      [修饰符] 子类名(外部类名 外部类实例,实参){
        外部类实例名.super(实参);
        ...
      }
      ...
    }
    

    示例如下:

    public class SubClass extends Out.In{
    	public SubClass(Out obj){
        	obj.super("hello");
        }
    }
    

    非静态内部类对象和其子类对象都必须持有寄生的外部类对象的引用,区别是创建两种对象时传入外部类对象的方式不同:当创建非静态内部类对象时,通过外部类对象来调用new关键字;当创建内部类子类对象时,将外部类对象作为子类构造器的参数。

    非静态内部类的子类实例仍然需要保留一个引用,即如果一个非静态内部类的子类的对象存在,也一定存在一个寄生的外部类对象。

    6.3.3.2 定义静态内部类的子类

    因为调用静态内部类的构造器时无需使用外部类对象,所以创建静态内部类的子类比较简单,格式如下:

    clsss 子类名 extends 外部类名.内部类名{
      ...
    }
    

    可以看出,当定义一个静态内部类时,其外部类非常像一个包空间。

    相比之下,使用静态内部类比使用非静态内部类简单很多,只要把外部类当成静态内部类的包空间即可,因此当程序需要使用内部类时,应该优先考虑使用静态内部类。

    外部类的子类中如果定义一个与父类内部类同名的内部类时,子类创建的是子类内部类的对象,父类创建的是父类内部类的对象,如果把子类对象赋给父类引用,再创建内部类对象,此时创建的是父类内部类的对象。可以把内部类看成事外部类的成员变量,通过静态分派确定符号引用。

    6.3.4 局部内部类

    如果在方法里定义内部类,则这个内部类是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在此方法以外的地方使用,因此局部内部类不需要访问控制符和static修饰符修饰。

    对于局部成员而言,不管是局部变量还是局部类,他们的上一级程序单元都是方法,而不是类,使用static修饰他们没有任何意义;不仅如此,因为局部成员的作用域是所在方法,其他程序单元永远也不可能访问一个方法中的局部成员,所以,所有的局部成员不能使用访问控制符和static修饰符。

    如果需要用局部内部类定义变量、创建实例或派生子类,那么都只能在局部内部类所在的方法内进行。

    public class LocalInnerClass{
      public static void main(String args){
        class InnerBase{
          int a;
        }
        class InnerSub extends InnerBase{
          int b;
        }
        InnerSub is = new InnerSub();
        is.a = 5;///////////////////////////////方法中可以直接访问局部内部类的域。
        is.b = 8;
        System.out.println(is.b + " " + is.a);
      }
    }
      
    

    编译程序,生成三个class文件:LocalInnerClass.class、LocalInnerClass$InnerBase.class、LocalInnerClass$InnerSub.class。注意到局部内部类的class文件的文件名比成员内部类的class文件的文件名多了一个数字,这是因为同一个类里不可能有两个同名的成员内部类,而同一个类里可能有两个以上的同名的局部内部类(处于不同的方法中)。

    局部内部类在实际开发中很少使用,因为局部内部类的作用域太小了,只能在当前方法中使用。

    6.3.5 匿名内部类

    匿名内部类适合创建只需要使用一次的类,创建匿名内部类时会立即创建一个该类的实例。定义格式如下:

    new 实现接口() |父类构造器(实参列表)
    {
      //匿名内部类的类体部分
    }
    

    从定义可知,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类或实现一个接口。

    关于匿名内部类有如下两条规则:

    • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。

    • 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器。取而代之的是,将构造器参数传递给父类构造器。同时匿名内部类可以定义初始化块,可以通过实例初始化块完成构造器需要完成的事情。

    最常用的创建匿名内部类的方式是需要创建某个接口类型的对象。

    interface Product{ 
      public String getName();
    }
    public class AnonyTest{
      public void test(Product p){
        System.out.println("购买了" + p.getName());    
      }
      public static void main(String[] args){
        AnonyTest obj = new AnonyTest();
        obj.test( new Product(){
            public String getName(){
              return "tom";
            }                      
          });
      }
    }
    

    上述程序中的AnonyTest类定义了一个test方法,该方法需要一个Product对象作为参数,但Product只是一个接口,无法直接创建对象,因此考虑创建一个Product接口实现类的对象传入该方法---如果这个Product接口实现需要重复使用,则应该将实现类定义成一个独立类;如果这个Product接口实现类只需要一次使用,就可以定义一个匿名内部类。

    由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或接口里包含的所有抽象方法。

    当通过实现接口来创建匿名内部类时,由于结构没有构造器。因此new接口名后的括号里不能传入参数值。

    但是如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此处的相似指的是拥有相同的形参列表。

    匿名内部类继承抽象父类的示例:

    abstract class Device{
      private String name;
      public abstract double getPrice();
      public Device(){}
      public Device(String name){
        this.name = name;
      }
      //省略name的访问器和修改器
    }
    public class AnonyTest{
      public void test(Device p){
        System.out.println("花费" + p.getPrice());      
      }
      public static void main(String[] args){
      	AnonyTest obj = new AnonyTest();
        obj.test(new Device("honey"){
                   public double getPrice(){
                     return 56.3;
                   }  
        });
        Device p = new Device(){
          {//初始化块
            System.out.println("匿名内部类的初始化块:");
          }
          //实现抽象方法
          public double getPrice(){
            return 56.3;
          }
          //覆盖父类方法
          public String getName(){
    	  	return "键盘";
          }
        }
        obj.test(p);
      }
    }
    

    当创建匿名内部类时,必须实现接口或抽象父类里的所有抽象方法,如果有需要,也可以重写父类的普通方法。

    在java8之前,java要求被局部内部类、匿名内部类访问的局部变量,在方法中定义时必须用final修饰,从java8开始这个限制被取消了,由编译器进行处理:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰符。此局部变量在第一次赋值后,值不能再修改,否则编译器将报错。例如:

    public class PairTest{
        public static void main(String[] args){
            int age = 0 ;
            age =3;
            class Device{
                void test(){
                    System.out.println(age);
                }
            }
            Device d = new Device();
            d.test();
        }
    }
    

    age在初始化为0后,被赋值为3,所以编译器将会报错。

    java8将这个功能称为“effective final”,意思是对于匿名内部类访问的局部变量,可以用final修饰,也可以不同final修饰,但必须按照有final修饰的方式来用,也就是一次赋值后,以后不能重新赋值。

    内部类是一种编译器现象,与虚拟机无关。编译器会把内部类翻译成用$分隔外部类名与内部类名的常规类文件,而这个操作对虚拟机是透明的。编译阶段,编译器会对内部类进行处理,转化为外部类,内部类对外部类对象的访问是因为编译器会在内部类的构造器中添加一个外部类引用的参数。内部类对外部类实例域的访问:编译器会在外部类中添加相关实例域的访问器方法,从而内部类对外部类实例域的访问将转化为调用外部类的访问器方法来实现。

    局部内部类对方法内的局部变量的访问:由于方法在创建局部内部类实例后,可能程序执行结束,局部变量会释放,此时局部内部类中将无法访问到局部变量,所以编译器会在局部内部类中添加实例域,在创建局部内部类实例时,将局部变量的值保存到添加的实例域中。

    在内部类不需要访问外部类对象时,应该使用静态内部类。

    6.4 Lambda表达式

    6.4.1 Lambda表达式的定义

    函数式接口:只有一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。

    匿名内部类的实现,以及Lambda表达式使用示例:

    public class CommandTest{
      public static void main(String[] args){
        ProcessArray pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        pa.process(target, new Command(){
          public int process(int[] target){
            int sum = 0;
            for(int tmp : target){
              sum += temp;
            }
            return sum;
          }     
        });
      }
    }
    

    Lambda表达式可以对上述代码进行简化:

    public class CommandTest{
      public static void main(String[] args){
        ProcessArray pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        pa.process(target, (int[] target)->{     
            int sum = 0;
            for(int tmp : target){
              sum += temp;
            }
            return sum;
          });         
      }
    }
    

    可以看出,Lambda表达式的作用就是简化匿名内部类的繁琐语法。它有三部分构成:

    • 形参列表。形参列表允许省略形参类型。如果形参类别中只有一个参数,甚至连形参列表的圆括号都可以省略。
    • 箭头(->)。必须通过英文中画线号和大于符号组成。
    • 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号。同时,当Lambda代码块只有一条return语句时,甚至可以省略return关键字。此时,如果lambda表达式需要返回值,而它的代码块中只有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。

    总结起来,lambda共有如下几种省略情况:

    • 可以省略参数类型。
    • 只有一个参数时,可以省略参数的圆括号
    • 语句体只有一条语句时,可以省略花括号,此时语句末尾的分号也省略。
    • 只有一条return语句时,可以省略return关键字,语句末尾的分号也省略。

    示例如下:

    interface Flyable{
      void fly(String weather);
    }
    public class LambdaQs{ 
      public void drive(Flyable f){
        System.out.println("我正在驾驶:" + f);
        f.fly("晴天");
      } 
      public static void main(String[] args){
        LambdaQs lq = new LambdaQs();    
        lq.drive(weather -> 
                 {
                   System.out.println("今天天气是:" + weather);	
                   System.out.println("直升机飞行平稳");
                 });   
      }
    }
    
    6.4.2 Lambda表达式与函数式接口

    定义Lambda表达式时即创建了一个对象,对象的类型要求是一个函数式接口,具体由赋值号左边的操作数类型决定。可以使用Lambda表达式进行赋值。用Lambda表达式进行赋值的示例如下:

    Runnable r = ()->{
      for(int i = 0;i < 100 ; i++){
        System.out.println();
      }
    };
    

    Lambda表达式的限制如下:

    • Lambda表达式的目标类型必须是明确的函数式接口,甚至不能赋给Object类型的变量,否则,无法确定lambda表达式的运行时类型。
    • Lambda表达式只能实现一个方法,因此它只能为函数式接口创建对象。

    示例如下:

    Object obj = ()->{
     	for(int i = 0;i < 100 ; i++){
        	System.out.println();
      	}
    };
    

    上述代码的Lambda表达式赋给的是Object对象而不是函数式接口。所以,编译器会报错。

    为了保证Lambda表达式的目标类型是明确的函数式接口,可以有如下三种常见方式:

    • 将Lambda表达式赋值给函数式接口类型的变量
    • 将Lambda表达式作为函数式接口类型的参数传给某个方法。
    • 使用函数式接口对Lambda表达式进行强制类型转换。

    因此上述代码,可修改

    Object obj = (Runnable)()->{
     	for(int i = 0;i < 100 ; i++){
        	System.out.println();
      	}
    };
    

    易知,Lambda表达式的目标类型完全可能是变化的(即可能会利用强制类型转换,将Lambda表达式赋给另一个抽象方法相同的接口变量),唯一的要求是,Lambda表达式实现的匿名方法与函数式接口中的抽象方法有相同的形参列表和返回值。示例如下:

    interface FKTest{
      public void run();
    }
    Runnable obj = ()->{
     	for(int i = 0;i < 100 ; i++){
        	System.out.println();
      	}  
    };
    FKTest fk = (FKTest)obj;//赋值合法
    

    Java 8在java.util.function包下预定义了大量的函数式接口,典型 地包含如下4类接口。

    • XxxFunction:这类接口中通常包含一个apply()抽象方法,该方法对参数进行处理、转换,然后返回一个新的值。该函数式接口通常用于对指定的数据进行转换处理。
    • XxxConsumer:这类接口中通常包含一个accept()抽象方法,该方法也负责对参数进行处理,只是该方法不会返回处理结果。
    • XxxPredicate:这类接口中通常包含一个test()抽象方法,该方法通常用来对参数进行某种判断,然后返回一个boolean值。该接口通常用于判断参数是否满足特定条件,经常用于涮选数据。
    • XxxSupplier:这类接口中通常包含一个getAsXxx()抽象方法,该方法不需要输入参数,该方法会按某种逻辑算法返回一个数据。

    综上所述,不难发现Lambda表达式的本质很简单,就是使用简洁的语法来创建函数式接口的实例------这种语法避免了匿名函数类的繁琐。

    6.4.3 方法引用与构造器引用

    有时,现有方法可以完成抽象方法的功能,此时可以直接调用 现有类的方法或构造器,称为方法引用和构造器引用。

    方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。Lambda表达式支持如下表所示的几种引用方式。

    种类 示例 说明 对应的Lambda表达式
    引用类方法 类名::类方法 函数式接口中被实现方法的全部参数传给该类方法作为参数 (a,b,...)->类名.类方法(a,b,...)
    引用特定对象的实例方法 特定对象::实例方法 函数式接口中被实现方法的全部参数传给该类方法作为参数 (a,b,...)->特定对象.实例方法(a,b,...)
    引用某类对象的实例方法 类名::实例方法 函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数 (a,b,...)->a.实例方法(a,b,...)
    引用构造器 类名::new 函数式接口中被实现方法的全部参数传给该构造器作为参数 (a,b,...)->new 类名(a,b,...)
    6.4.3.1 引用类方法
    @FunctionInterface
    interface Converter{
      Ingeter convert(String from);
    }
    //使用Lambda表达式创建Conveter对象
    Converter converter1 = from -> Integer.ValueOf(from);
    Integer val = converter1.convert("99");
    

    上面代码调用converter1对象的convert()方法时------由于converter1对象是由Lambda表达式创建的,convert()方法执行体就是Lambda表达式的代码部分。

    上述的Lambda表达式的代码块只有一行调用类的方法的代码,因此可以替换为:

    Converter converter1 = Integer::ValueOf;
    
    6.4.3.2 引用特定对象的实例方法
    Converter converter2 = from -> "fkit.org".indexOf(from);
    Integer value = converter1.convert("it");
    

    上述的Lambda表达式的代码块只有一行调用"fkit.org"的indexOf()实例方法的代码,因此可以替换为:

    Converter converter2 = "fkit.org"::indexOf;
    
    6.4.3.3 引用某类对象的实例方法
    @FunctionalInterface
    interface MyTest{
      String test(String a, int b, int c);
    }
    MyTest mt = (a, b, c)-> a.subString(b, c);
    String str = mt.test("Java I love you", 2,9);
    

    上述的Lambda表达式的代码块只有一行a.subString(b, c);因此可以替换为:

    MyTest mt = String::subString;
    
    6.4.3.4 引用构造器
    @FunctionInterface
    interface YourTest{
      JFrame win(String title);
    }
    YourTest yt = (String a)->new JFrame(a);
    JFrame jf = yt.win("我的窗口");
    

    上述Lambda表达式的代码块只有一行new JFrame(a);因此可以替换为:

    YourTest yt = JFrame::new;
    

    方法引用和构造器引用中,如果有多个同名的重载方法,编译器会依据表达式实际转换的函数式接口中声明的方法进行选择。可以使用数组类型建立构造器引用,例如int[]::new,它有一个参数,即数组的长度,这等价于lambda表达式x->new int[x]

    可以在方法中使用this参数,例如this::equals等同于x-> this.equals(x),使用super也是合法的。this表示lambda表达式所在方法的对象。例如:

    class Greeter{
      public void greet(){
        System.out.println("hello world!");
      }
    }
    class TimedGreeter extends Greeter{
      public void greet(){
        Timer t = new Timer(1000, super::greet);
        t.start();
      }
    }
    
    6.4.4 Lambda 表达式与匿名内部类的联系和区别

    Lambda 表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用,Lambda 表达式与匿名内部类存在以下相同点:

    • Lambda 表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量);
    • Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
    interface Displayable{
      void display();
      default int add(int a, int b){
        return a + b;
      }
    }
    public class LabdaAndInner{
      private int age = 12;
      private static String name = "i'm here";
      public void test(){
          String book = "疯狂java";
          Displayable dis = ()->{
            System.out.println("book 局部变量为:" + book);
            System.out.println("age 局部变量为:" + age);  
            System.out.println("name 局部变量为:" + name);  
        }
        dis.display();
        System.out.println(dis.add(3, 5));
      }
    }
    

    上述代码示范了Lambda表达式分别访问“effective final”的局部变量、外部类的实例变量和类变量。Lambda表达式访问局部变量时,编译器隐式为Lambda表达式添加一个私有域常量,并将局部变量放入Lambda表达式的默认构造器中以初始化私有域常量。Lambda表达式对于外部类实例域的访问是编译器将外部类实例引用作为参数传入Lambda表达式的默认构造器,同时在Lambda表达式中定义一个实例域保存外部类实例引用实现的。

    Lambda表达式与匿名内部类的主要区别:

    • 匿名内部类可以为任意接口创造实例;不管接口有多少个抽象方法们只要匿名内部类实现了所有的抽象方法即可;但是Lambda表达式只能为函数式接口创建实例。
    • 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
    • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用接口中定义的默认方法。因为定义匿名内部类时已知其实现的接口或父类,而定义Lambda表达式时,其目标类型未知,虽然可以通过赋值号左边的类型推断,但由于此Lambda表达式还可以赋给定义了相同抽象方法的其他函数式接口,而那些接口中未必也定义了相同的默认方法。

    例如在Lambda表达式的代码块中增加如下一行,编译器将会报错。

    System.out.println(add(3, 5));
    

    java中,lambda表达式就是闭包,如果在lambda表达式中使用了所在方法中的局部变量,称lambda表达式捕获了此局部变量。易知被捕获的局部变量都必须是effectively final(最终变量,即变量初始化之后就不会再为它赋新值),且在lambda表达式中也不能改变,否则,当多个变量同时引用此lambda表达式时,会出现并发的安全问题。

    lambda表达式的体与嵌套块有相同的作用域。同样适用命名冲突和遮蔽的规则。因此lambda表达式中不能声明与局部变量同名的参数或局部变量。

    使用lambda表达式的重点是延迟执行。

    在设计接口时,如果接口中只有一个抽象方法,就可以用@FunctionInterface来标记这个接口。这样,如果无意中增加了另一个非抽象方法,编译器会产生一个错误的信息。

    6.5 垃圾回收

    垃圾回收机制具有如下特点:

    • 垃圾回收机制只负责回收堆内存中的对象,不会回收物理资源。
    • 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统就会在合适的时候回收它所占的内存。
    • 在垃圾回收机制回收任何对象之前,总会调用它的finalize()方法,该方法可能使该对象复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。
    6.5.1 对象在内存中的状态

    当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:

    • 可达状态:当一个对象被创建后,若有引用变量引用它,则该对象处于可达状态。
    • 可恢复状态:如果某个对象不再有任何引用变量引用它,就处于可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象占用的内存,在回收该对象之前,系统会调用对象的finalize()方法进行资源清理。如果系统在调用所有可恢复对象的finalize()时重新让一个引用变量引用该对象,则这个对象再次变为可达状态;否则该对象将进入不可达状态。
    • 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后,依然没有使该对象变为可达状态,那么该对象将永久性地失去引用,变成不可达状态。只有当对象处于不可达状态时,系统才会真正回收该对象占有的资源。
    6.5.2 强制垃圾回收

    当一个对象失去引用后,系统何时调用它的finalize()对其进行资源清理,它何时变为不可达状态,系统何时回收它占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,决不能控制它何时被回收。

    程序无法精确控制Java垃圾回收的时间,但依然可以强制系统进行垃圾回收--这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。强制系统垃圾回收有如下两种方式:

    • 调用System类的静态方法gc():System.gc()
    • 调用Runtime对象的实例方法gc()Runtime.getRuntime().gc()

    示例如下:

    public class GcTest{
      public static void main(String[] args){
        for(int i = 0; i < 4; i++){
          new GcTest();
          //下面两种方法完全相同
          System.gc();
          //Runtime.getRuntime().gc();
        }
      }
      public void finalize(){
        System.out.println("系统正在清理");
      }
    }
    
    6.5.3 finalize()

    finalize()是定义在Object类里的实例方法,方法原型为:

    protected void finalize() throws Throwable

    finalize()方法返回后,对象消失,垃圾回收机制开始执行。方法原型中的throws Throwable表示可以抛出任何异常。

    任何java类都可以重写Object类的finalize()方法,在该方法中清理对象占用的资源。只有当程序认为需要更多的额外内存时,垃圾回收机制才会进行垃圾回收。

    finalize()具有如下4个特点:

    • 永远不要主动调用某个对象的finalize()方法,该方法应交给垃圾回收机制调用。
    • finalize()方法何时被调用,是否被调用具有不确定性,不要把finalize()当成一定会被执行的方法。
    • JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重新变为可达状态。
    • JVM执行finalize()方法出现异常时,垃圾回收机制不会报告异常,程序继续执行。

    示例如下:

    public class FinalizeTest{
      private static final FinalizeTest ft = null;
      public void info(){
    	System.out.println("测试finalize方法");
      }
      public static void main(String[] args){
        new FinalizeTest();
        System.gc();
        System.runFinalization();
        ft.info();
      }
      public void finalize(){
        ft = this;
      }
    }
    

    代码中的finalize()把需要清理的可恢复对象重新赋给静态变量,从而让该可恢复对象重新变成可达状态。通常finalize()方法的最后一句是调用父类的finalize():super.finalize()

    6.5.4 对象的软、弱和虚引用

    对大部分对象而言,程序里会有一个引用变量引用该对象,这是最常见的引用方式。除此之外,java.lang.ref包下提供了3个类:SoftReference、PhantomReferenceWeakReference,他们分别代表了系统对对象的3种引用方式:软引用、弱引用和虚引用。因此Java语言对对象的引用有如下4种方式:

    1. 强引用(StrongReference)

      这是Java程序中最常见的引用方法。程序创建一个对象,并把这个对象赋给一个引用变量。当一个对象被引用变量引用时,它处于可达状态,不可能被垃圾回收机制回收。

    2. 软引用

      软引用需要通过SoftReference类来实现,当一个对象只有软引用时,它可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存足够时,它不会被回收,程序也可使用该对象;当系统内存不足时,系统可能会回收它。

    3. 弱引用

      弱引用通过WeakReference类实现,弱引用和软引用类似,但弱引用的引用级别更低。对于只有弱引用的对象而言,当垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象的内存。

    4. 虚引用

      虚引用通过PhantomReference类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有虚引用时,那么它和没有引用效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。

    上面三个引用类都包含一个get()方法,用于获取他们引用的

    对象。但虚引用太弱了,无法获取到引用的对象。

    引用队列ReferenceQueuejava.lang.ref.ReferenceQueue类表示,用于保存被回收后对象的引用。当联合使用软引用、弱引用和虚引用时,系统在回收被引用的对象之后,将把被回收对象的引用添加到关联的引用队列中。

    软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序通过检查与虚引用关联的引用队列是否包含了该虚引用,从而了解虚引用所引用的对象是否即将被回收。

    弱引用用法示例:

    class Test{
      public static void main(String[] args){
        String str = new String("java");
        //创建弱引用,使其引用str对象
        WeakReference wr = new WeakReference(str);
        str = null;
        //取出弱引用wr所引用的对象
        System.out.println(wr.get());
        System.gc();
        System.runFinalization();
        //输出结果为null,表示对象已被回收
        System.out.println(wr.get());
      }
    }
    

    虚引用和引用队列用法示例:

    class Test{
      public static void main(String[] args){
        String str = new String("java");
        RefenceQueue rq = new RefenceQueue();
        //创建虚引用,使其引用str对象
        PhantomReference pr = new PhantomReference(str,rq);
        str = null;
        //取出虚引用wr所引用的对象,此处并不能获取虚引用所引用的对象
        System.out.println(pr.get());
        System.gc();
        System.runFinalization();
        //垃圾回收后,虚引用将被放入引用队列
        //取出引用队列中最先进入队列的引用与pr比较
        System.out.println(rq.poll() == pr);
      }
    }
    

    使用这些引用类就可以避免在程序执行期间将对象留在内存中。如果以软引用、弱引用或虚引用的方式引用对象,垃圾回收器就能随机地释放对象。

    由于垃圾回收的不确定性,当程序希望从软、弱引用中取出引用对象时,可能这个对象已经被释放。如果程序需要使用被引用的对象,则必须重新创建该对象。这个过程有如下两种方式:

    obj = wr.get();
    if(obj == null){
      wr = new WeakRefence(recreatIt());////  1
      obj = wr.get();////////      2
    }
    //操作对象obj
    //再次切断obj与对象的关联
    obj = null
    //方法二:
    obj = wr.get();
    if(obj == null){
      obj = recreatIt();
      wr = new WeakRefence(obj);
      //操作对象obj
      //再次切断obj与对象的关联
      obj = null
    }
    

    第一种方法,若垃圾回收机制在代码1和2之间回收了弱引用的对象,那么obj仍可能为null。而方法二不会出现这种情况。

  • 相关阅读:
    [java] 怎么去掉小数点后面不需要的0
    [SoapUI] 在SoapUI script里获取Response(Json格式)某个节点值
    nacos启动不停打印日志[com.alibaba.nacos.client.naming.updater] INFO com.alibaba.nacos.client.naming:192
    sppringcloud应用启动 访问gateway无法path无法路由到目标应用,404
    springcloud项目启动gateway报错org.springframework.cloud.gateway.config.GatewayAutoConfiguration required a bean of type 'org.springframework.http.codec.ServerCodecConfigurer' that could not be found
    nacos 01
    微服务spring-cloud day2
    springcloud本地启动指定profile后错误If you are using the git profile, you need to set a Git URI in your configuration.
    微服务spring-cloud day1
    金融云部署sofaboot应用指定了项目路径健康检查无法通过,windows格式/unix格式/mac格式的坑
  • 原文地址:https://www.cnblogs.com/echie/p/9867872.html
Copyright © 2020-2023  润新知