这里## 6.6 Java 9改进的接口

    • 6.6.1 接口的概念
    • 6.6.2 Java 9中接口的定义
    • 6.6.3接口的继承
      • ==以下代码中纯在自己的很多错误==
    • 6.6.4使用接口
    • 6.6.5接口和抽象类
    • 6.6.6面向接口编程
      • 1.简单工程模式
      • 2.命令模式
  • 6.7 内部类
    • 6.7.1非静态内部类
    • 6.7.2静态内部类
    • 6.7.3使用内部类
    • 6.7.4 局部内部类
    • 6.7.5 匿名内部类
  • 6.8 Java 11增强的Lambda表达式
    • 6.8.1 Lambda表达式入门
    • 6.8.2 Lambda表打死与函数式接口
  • 6.9 枚举类
    • 6.9.1 手动实现枚举类
    • 6.9.2 枚举类入门
    • 6.9.3 枚举类的成员变量、方法和构造器
    • 6.9.4 实现接口的枚举类
    • 6.9.5 包含抽象方法的枚举类
  • 6.10 对象与垃圾回收
    • 6.10.1 对象在内存中的状态
    • 6.10.2 强制垃圾回收
    • 6.10.3 finalize方法
    • 6.10.4 对象的软、弱和虚引用
  • 6.11 修饰符的使用范围
  • 6.12 多版本JAR包

抽象类是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类”——接口(interface)。Java 9对接口进行了改进,允许在接口中定义默认方法和类方法,默认方法和类方法都可以提供方法实现,Java 9为接口增加了一种私有方法,私有方法也可以提供方法实现。

6.6.1 接口的概念

同一个类的内部状态数据、各种方法的实现细节完全相同,类是一种具体实现体,而接口定义了一种规范,接口定义了某一批类所需要遵守的规范,接口u不关心内部状态数据,也不关心这些类里方法的实现细节,他只规定这批类里必须提供某些方法,提供这些方法的类就课满足满足实际需求。

可见,接口是从多个相似类中抽象出来的规范,接口不提供任何实现,接口体现的是规范和实现分离的设计学。

让规范和实现风力正是接口的好处,让软件系统的各组件之间面向对象接口耦合,是一种松耦合的设计,软件系统的各模块之间之间也应该采用这种面向接口的耦合,从而尽量降低各模块之间的耦合,为系统提供更好的可拓展性和维护性。

6.6.2 Java 9中接口的定义

与定义类不同,定义接口不再使用class关键字,而是使用interface关键字

[修饰符] interface 接口名 externds 父接口1,父接口2,···
{零到多个常量定义;零到多个抽象方法定义;零到多个内部类、接口、枚举定义;零到多个私有方法、默认方法或类方法定义;
}
  • 修饰符可以是public,也可以省略,省略后则默认为采用包权限访问控制符,即默认采用包权限访问控制符,即只有在相同结构下才可以访问该接口。
  • 从语法角度来看,接口名为合法的标识符即可,从可读性来看,可加形容词,每个单词的首字母大写。
  • 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。

接口中包含成员变量(只能是静态常量),方法(只能是抽象实例方法、类方法、默认方法或私有方法),内部类(包含内部接口、枚举)。不包含构造器和初始化块。权限全部为public。

Java 9为接口增加了一种新的私有方法,其实私有方法的主要作用就是做为工具方法,为接口中的默认方法或类方法提供支持。私有方法可以拥有方法体,私有方法既可以是类方法(用static修饰),也可以是实例方法。

对于定义静态常量而言,系统会自动添加public static final修饰符,而且接口里没有构造器和初始化块,所以静态常量需要在定义时就指定默认值。

接口中定义的方法只能是抽象方法,类方法、默认方法(实例方法)或私有方法;因此如果不是定义默认方法、类方法或私有方法,系统将自动为普通方法增加abstract修饰符;定义接口里的普通方法时,不管是否使用public abstract修饰符,接口里的普通方法总是使用public abstract来修饰。接口里的普通方法不能有方法实现(方法体),但类方法,私有方法、默认方法都必须有方法体。(也就是说,无普通方法,系统会自动转换为抽象方法,而且抽象方法本身就不可定义方法体。

总结:

​ 接口里定义内部类、内部接口、内部枚举,默认采用public static 两个修饰符。

​ 接口里定义普通方法默认使用public abstract修饰符,转换为抽象方法。

​ 接口里定义静态常量默认使用public static final修饰符,并要定义时指定初始值。

​ 接口里定义默认方法(实例方法)使用 public default修饰。

​ 接口里定义类方法用public static,系统会自动添加public。

package lee;public interface Output
{//  接口里定义的成员变量只能是常量int MAX_CACHE_LINE = 50;  //等价于 public static final int MAX_CACHE_LINE = 50;
//  接口里定义的普通方法只能是public的抽象方法void out();//等价与 public abstract void out();void getDate(String msg);// 等价与 public abstract void getDate(String msg);//因为是抽象方法,所以不能有方法体。
//  在接口中定义默认方法,需要使用default修饰default void print(String... msgs){for (var msg:msgs)  //看不懂{System.out.println(msg);}}
//  在接口中定义默认方法,需要使用default修饰default void test(){System.out.println("默认的test()方法");}
//  在接口中定义类方法,需要使用static修饰static String staticTest(){return "接口里的类方法";}
//  定义私有方法private void foo(){System.out.println("foo私有方法");}
//  定义私有静态方法private static void bar(){System.out.println("bar私有静态方法");}
}

​ 接口里定义默认方法(实例方法)使用 public default修饰。由于不能使用static,所以不能直接使用接口来调用默认方法,需要使用接口的实现类的实例来调用这些默认方法。

package yeeku;public class OutoutFieldTest
{public static void main(String[] args){//      访问另一个包中的Output接口的常量System.out.println(lee.Output.MAX_CACHE_LINE);
//      下面编译将引发“为final变量赋值”编译异常
//      lee.Output.MAX_CACHE_LINE = 20;
//      使用接口调用类方法System.out.println(lee.Output.staticTest());}
}

从以上代码中可以看出,两个包之间访问,表名该成员变量时public访问权限的,而且可以通过接口访问成员变量,表名这个成员变量是一个类变量。

6.6.3接口的继承

接口的继承与类的继承不同的是,接口完全支持多继承,即一个接口可以有多个直接父接口。

相似的是,子接口继承某个父接口,将会得到父接口里定义的所有抽象方法、常量。

一个接口继承多个父接口时,多个父接口排在extends关键词之后,多个父接口之间以 . 隔开。

以下代码中纯在自己的很多错误
interface InterfanceA
{//  定义了一个静态变量和抽象方法int PROP_A = 5;//public static finalvoid testA();//public abstract
}
interface InterfanceB
{int PROP_B = 6;void testB();static void testD(){System.out.println("在接口中定义除抽象方法的其他方法外都需要定义方法体。");}
}
interface InterfaceC extends InterfanceA,InterfanceB
{int PROP_C = 7;void testC();
}
public class InterfaceExtends
{public static void main(String[] args){System.out.println(InterfaceC.PROP_A);System.out.println(InterfaceC.PROP_B);System.out.println(InterfaceC.PROP_C);
//      下面编译将会引起“没有为类型 InterfaceC 定义方法 testD”的错误
//      原因是接口在继承中只继承父类的抽象方法和常量,不继承类方法等···
//      System.out.println(InterfaceC.testD());
//      下面编译将会引起“ 类型 PrintStream 中的方法 println(boolean)对于参数(void)不适用”的错误
//      不能够输出方法,只能够调用方法。
//      System.out.println(InterfanceB.testD());InterfanceB.testD();}
}

6.6.4使用接口

接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其他现实类的对象。除此之外,接口的主要用途就是被实现类实现。归纳起来,接口主要有如下用途:

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

一个类可以实现一个或多个接口,继承使用extends关键字,实现则用implements关键字。因为一个类可以实现多个接口,这也就是Java为单继承灵活性不足所做的补充。

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

实现接口与继承父类相似,一样可以获得所实现接口里定义的常量(成员变量)和方法(抽象方法,私有方法(public default修饰))。

一个类可以继承一个父类,并同时实现多个接口,implements部分必须放在extends部分之后。

一个类实现了一个或多个接口后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。

一个类实现一个接口可以看成一个类继承了一个彻底抽象的类。下面看一个实现接口的类:

Printer.java

import lee.Output;interface Product
{int getProduceTime();
}
public class Printer implements Output,Product
{private String[] printData = new String[MAX_CACHE_LINE];
//  用以记录当前需要打印的作业数private int dataNum = 0;
//  实现实现 lee.Output.out()public void out(){//      只要还有作业,就继续打印while(dataNum > 0){System.out.println("打印机打印:" + printData[0]);
//          把作业队列整体前移一位,并将剩下的作业数减1System.arraycopy(printData,1,printData,0,--dataNum);}}
//  实现 lee.Output.getDatepublic void getDate(String msg){if(dataNum >= MAX_CACHE_LINE){System.out.println("输出队列已满,添加失败。");}else{//          把答应数据添加到队列里,已保存的数据加1printData[dataNum++] = msg;}}
//  实现getProduceTimepublic int getProduceTime(){return 45;}public static void main(String[] args){//      创建一个Printer对象,当成Output使用Output o = new Printer();o.getDate("轻量级Java EE企业级应用实战");o.getDate("疯狂Java讲义");o.out();
//      调用Output接口中定义的默认方法o.print("孙悟空","猪八戒","白骨精");o.test();
//      创建一个Printer对象,当成Product使用Product p = new Printer();System.out.println(p.getProduceTime());
//      所有接口类型的引用变量都可以直接赋给Object类型变量Object obj = p;}
}

Printer对象实现了Output接口和Product接口,因此Printer对象即可直接赋给Output变量,也可直接赋给Product变量。仿佛Printer类既是Output类的子类,也是Product类型子类,这就是Java提供的模拟多继承。

实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口里的方法时只能能使用public修饰。

接口不能显式的继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。所以在上面程序中可以把Product类型的变量直接赋给Object类型变量,这就是利用向上转型实现的,因为编译器知道任何Java对象都必须时Object或其他子类的实例,Product类型的对象也不例外(它必须时Product接口实现类的对象,该实现类肯定时Object的显式或隐式子类)。

6.6.5接口和抽象类

相似特征:

  • 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
  • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

不同点:

  • 接口:

    对于实现者而言

    ​ 接口规定了实现者必须向外提供服务(以方法的形式来提供)

    对于调用者而言

    ​ 接口规定了调用者可以调用那些服务,以及如何调用这些服务(就是如何来调用方法)。

    ​ 当一个程序使用接口时,接口是多个模块间的耦合标准。

    ​ 当多个应用程序之间使用接口时,接口是多个程序之间的通信标准。

    接口类似于整个程序的总纲,因此不要随意修改接口,一旦接口被改变,导致系统中大部分类都需要重写。

  • 抽象类:

    作为系统中多个子类的父类,所体现的是一种模板式设计。·需要进行进一步的完善。

除此之外,用法上纯在如下差别:

①接口里只能包含抽象方法、静态方法、私有方法(类方法)和默认方法,不能为普通方法提供实现;抽象类则完全可以包含普通方法。

②接口里之恶能定义静态常量,不能定义普通常量;抽象类里则既可以定义普通成员变量,也可定义静态常量。

③接口里不包含构造器;抽象类里可以包含构造器,但是不可以用来创建对象,而是让其子类去调用构造器来完成属于抽象类的初始化操作。

④接口里不能包含初始化块,抽象类里可以。

⑤一个类最多只能由一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

6.6.6面向接口编程

​ 接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好的降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。

基于这种原则,很多软件架构设计理论都倡导“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。下面介绍两种设计模式来示范面向接口编程的优势。

1.简单工程模式

有一个场景:假设程序中有个Computer类需要组合一个输出设备,现在又两个选择:直接让Computer类组合一个Printer,或则让Computer类组合一个Output,那么到底哪种好呢。

工厂模式建议让Computer类组合一个Output类型的对象,将Computer类与Printer类完全分离。Computer对象实组合的是Printer对象还是BetterPrinter对象,对Computer完全透明。当Printer对象切换到BetterPrinter对象时,系统完全不受影响。

import lee.Output;public class Computer
{private Output out;public Computer(Output out){this.out = out;}
//  定义一个模拟获取字符串输入的方法public void keyIn(String msg){out.getData(msg);}
//  定义一个模拟打印的方法public void print(){out.out();}
}

​ 上面的Computer类已经完全与Printer类分离,只是与Output接口耦合。Computer不再负责创建Output对象,系统提供一个Output工厂来负责生成Output对象。这个OutputFactory工厂类代码如下:

import lee.Output;public class OutputFactory
{public Output getOutput(){//创建了一个Printer对象return new Printer();}public static void main(String[] args){var of = new OutputFactory();var c = new Computer(of.getOutput());c.keyIn("轻量级");c.keyIn("疯狂");c.print();}
}

在该OutputFactory类中包含了一个getOutput()方法,该方法返回一个Output实现类的实例,该方法负责创建Output实例,具体创建哪一个实现类的对象由该方法决定(具体由该方法的return new Printer()控制,当然也可以增加更复杂的控制逻辑)。如果系统需要将Printer改为BetterPrinter实现类,只需要让BetterPrinter实现Output接口,并实现OutputFacyory类中的getOutput()方法即可。

下面时BetterPrinter实现类的代码,BetterPrinter只是对原有的Printer进行简单修改,以模拟系统重构后的改进。

import lee.Output;public class BetterPrinter implements Output
{private String[] printData= new String[MAX_CACHE_LINE * 2];
//  用以记录当前需打印的作业数private int dataNum = 0;public void out(){//      只要还有作业,就继续打印while (dataNum > 0){System.out.println("高速打印机正在打印:" + printData[0]);
//          把作业队列整体前移一位,并将剩下的作业数减1System.arraycopy(printData, 1, printData, 0, --dataNum);}}public void getData(String msg){if(dataNum >= MAX_CACHE_LINE * 2){System.out.println("输出队列已满,添加失败");}else{//          把打印数据添加到队列里,已保存数据的数量加1printData[dataNum++] = msg;}}
}

上面的BetterPrinter类也实现了Output接口,因此也可当成Output接口来使用,于是只要把OutputFactory工厂类的getOutput()方法中粗体部分改为如下代码:

return new BetterPrinter();

再次运行OutputFactory.java,**发现系统运行时已经改为BetterPrinter对象,**而不再是原来的Printer对象。

通过这种方法,即可把所有生成的Output对象的逻辑集中在OutputFactory工厂类模式中管理,而所有需要使用Output对象的类只需要与Output接口耦合,而不是具体的实现类耦合。即使系统中有很多类使用了Printer对象,只要OutputFactory类的getOutput()方法生成的Output对象是BetterPrinter对象,则它们全部可以改为这个对象,而所有程序无需修改,只需要修改工厂类的getOutput方法实现即可。

2.命令模式

一个场景:某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行改方法时才可以确定。具体点:假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的某个行为。

对于这样一个需求,必须把“处理行为”作为参数传入该方法,这个“处理行为”用编程来实现就是一段代码可以考虑使用一个Command接口来定义一个方法,用这个方法来封装“处理行为”。下面是该接口的代码。

=public interface Command
{//  接口里定义的process方法用于封装“处理行为”void process(int element);
}

定义了一个process()方法,这个方法用于封装“处理行为”,但这个方法没有方法体——因为现在还无法确定这个处理行为。

下面是需要处理数组的处理类,在这个处理类中包含一个process()方法,这个方法无法确定处理数组的处理行为,所以定义该方法时使用了一个Command参数,这个Command参数负责对数组的处理行为。

public class ProcessArray
{public void process(int[] target,Command cmd){for(var t:target)  //多态{cmd.process(t);}}
}

通过一个Command接口,就实现了让ProcessArray类和具体“处理行为”分离,程序使用Command接口代表了对数组元素的处理行为。Command接口也没有提供真正的处理,只有等到需要调用ProcessArray对象的process()方法时,才真正传入一个Command对象,才正在确定对数组的处理行为。

public class CommandTest
{public static void main(String[] args){var pa = new ProcessArray();int[] target = {3,-4,6,4};
//      第一次处理数组,具体处理行为取决于PrintCommandpa.process(target, new PrintCommand());System.out.println("------------------");pa.process(target, new SquareCommand());}
}
public class PrintCommand implements Command
{public void process(int element){System.out.println("迭代输出目标数组的元素"+element);}}
public class SquareCommand implements Command
{public void process(int element){System.out.println("数组元素的平方是:"+element * element);}
}

6.7 内部类

大部分的时候,类被定义成一个独立的程序单元,在某些情况下,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就称作内部类(有的地方叫嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)。有以下作用:

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
  • 内部类成员可以直接访问外部类的私有数据,因为内部类被当成外部类成员,同一个类的成员之间可以互相访问,但外部类不能访问内部类的实现细节,例如内部类的成员变量。
  • 匿名内部类适合创建仅需要创建一次使用的类。

内部类除需要定义在其他类里面还存在两点不同:

  • 内部类可以比外部类多三个修饰符private,protected,static——外部类不可以使用这三个修饰符。
  • 非静态内部类不能拥有静态对象。

6.7.1非静态内部类

大部分的时候,内部类都可以被当作成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;既不内部类和匿名内部类则不是类成员。

成员内部类分为:静态内部类和非静态内部类,内部类一定是放在另一个类的类体部分定义。

下面程序在Cow类中创建了CowLeg非静态内部类,并在CowLeg类的实例方法中直接访问Cow的private访问权限下的实例方法,

public class Cow
{private double weight;
//  外部类的两个重载的构造器public Cow() {}public Cow(double weight){this.weight = weight;}
//  定义一个非静态内部类private class CowLeg{//      非静态内部类的两个实例变量private double lenght;private String color;
//      非静态内部类的两个重载构造器public CowLeg() {}public CowLeg(double lenght,String color){this.lenght = lenght;this.color = color;}public double getLenght(){return lenght;}public void setLenght(double lenght){this.lenght = lenght;}public String getColor(){return color;}public void setColor(String color){this.color = color;}public void info(){System.out.println("当前牛腿的颜色是:"+color + "高:"+lenght);System.out.println("本牛腿所在奶牛重:"+weight);}}public void test(){var c1 = new CowLeg(1.12,"黑白相间");c1.info();}public static void main(String[] args){var c2 = new Cow(378.9);c2.test();}
}

生成两个class文件,内部类的class文件总是以OutClass$InnerClass.class。

在非静态内部类里可以直接访问外部类的private成员,System.out.println(“本牛腿所在奶牛重:”+weight);这行代码就是内部类直接访问外部类的private成员,这是因为在非静态内部类对象里,保存了一个他所寄生的外部类对象的引用(当调用非静态内部类的实例方法时(info()),必须有一个非静态内部类实例(c1)非静态内部类实例必须寄生在外部类实例里)。

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

因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可以通过外部类类名.this、this来限定区分。

public class DiscernVariable
{private String prop = "外部类的实例变量";private class Inclass{private String prop = "内部类的实例变量";public void info(){var porp = "局部变量";System.out.println("外部类的实例变量值:"+DiscernVariable.this.prop);System.out.println("内部类的实例变量值:"+this.prop);System.out.println("局部变量的值:"+porp);}}public void test(){var in = new Inclass();in.info();}public static void main(String[] args){new DiscernVariable().test();}
}

非静态的内部类成员可以访问外部类的实例成员,但反过来就不成立了。如果外部类需要访问非静态内部类的实例成员,则必须显式创建非静态内部类对象来调用访问其他实例成员。

public class Outer
{private int OutProp = 9;class Inner{private int inPror = 5;public void accessOuterPerp(){//          非静态内部类可以直接访问外部类的private实例变量System.out.println("外部类的OutPror值"+OutProp);}}public void accessInnerPerp(){//      外部类不可以直接访问内部类的实例变量
//      System.out.println("内部类的inProp值:"
//              +inProp);
//      必须创建显式内部类的对象来访问内部类的成员变量System.out.println("内部类的inProp值:"+ new Inner().inPror);}public static void main(String[] args){//      执行下面代码只会创建外部类对象,还未创建内部类对象Outer o1 = new Outer(); //①o1.accessInnerPerp();}
}

外部类不允许访问非静态内部类的实例成员的原因是,上面①号段代码创建了一个外部类对象,并调用外部类对象的accessInnerProp()方法,此时非静态内部类对象更不不存在,如果允许accessInnerProp()方法访问非静态内部类的实例成员,将肯定引起错误。

非静态内部类和外部类的关系:

非静态内部类必须寄生在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄生其中。简单的来说,如果存在一个非静态内部类对象,则一定存在一个被它寄生得外部类对象。但外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在!而非静态内部类对象访问外部类成员时,外部类对象一定存在。

根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量,创建实例等。总之,不允许在外部类的静态成员变量中直接用非静态内部类。

public class StaticTest
{//  定义一个非静态的内部类,是一个空类private class In{}
//  外部类的静态方法public static void main(String[] args){//      下面代码引发编译异常,因为静态成员(main()方法)
//      无法访问非静态成员(in类)
//      new In();}
}

Java不允许在非静态内部类里定义静态成员,非静态内部类里不能有静态方法、静态成员变量、静态初始化块,所以上面三个静态声明都会引发错误。

public class InnerNoStatic
{private class InnerClass{/** 下面三个静态声明都将引发如下编译错误* 非静态内部类不能有静态声明*/
//      static
//      {//          System.out.println("========");
//      }
//      private static int inProp;
//      private static void test() {}}
}

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

6.7.2静态内部类

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

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

静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。

public class StaticInnerClassTest
{private int prop1 = 5;private static int prop2 = 9;static class StaticInnerClass{//      静态内部类里可以包含静态成员private static int age;//①private void accessOutProp(){//          静态内部类无法访问外部类的实例变量
//          System.out.println(prop1);System.out.println(prop2);}}
}

①中定义了一个静态成员变量 ,因为这个静态成员变量处于静态内部类中,所以完全没有问题。StaticInnerClass类中定义了一个accessOutProp()方法,这是一个实例方法,但依然不能访问外部类的prop1成员变量,但可以访问prop2成员变量,因为它是静态成员变量。

静态内部类的实例方法不能访问外部类的实例属性,因为静态内部类是与外部类相关的,而不是外部类对象相关的,也就是说,静态内部类对象不是寄生在外部类的实例中,而是寄生在外部类的类本身中。当静态内部类对象存在时,并不存在一个被它寄生的外部类对象,静态内部类对象只持有外部类引用,没有持有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员,但找不到被寄生的外部类对象,这将引起错误。以下代码帮助理解。

public class Help
{public  int age;public static int wight;public static void yea(){//      不能对非静态字段 age 进行静态引用
//      age = 10;wight = 100;}
}

6.7.3使用内部类

1.在外部类内部使用内部类

2.在外部类以外使用非静态内部类

示范了如何在外部类以外的地方创建非静态内部类的对象,并把它赋给非静态内部类类型

//如何在外部类以外的地方创建非静态内部类的对象,并把它赋给非静态内部类类型变量。
class Out
{//  定义一个内部类,不使用访问控制符
//  即只有同一个包中的其他类可访问该内部类class In{public In(String msg){System.out.println(msg);}}
}
public class CreateInnerInstance
{public static void main(String[] args){//非静态内部类的构造器必须使用外部类对象来调用Out.In in = new Out().new In("测试信息");/** 上面的代码可改为如下三行代码* 使用OuterClass.InnerClass的形式定义内部类变量* Out.In in;  //调用内部类构造器* 创建外部类实例,非静态内部类实例将寄生在该实例中* Out out = new Out();* 通过外部类实例和new来调用内部类构造器创建非静态实例* in = out.new In("测试信息");*/}
}

如果需要在外部类以外的地方创建非静态内部类的子类,尤其要注意上面的规则:非静态内部类的构造器必须通过其外部类对象来调用。

当创建一个子类时,子类构造器总会调用父类构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象。下面定义了一个子类继承了Out类的非静态内部类In类。

public class SubClass extends Out.In
{public SubClass(Out out){//        调用父类构造器out.super("hello");}
}

非静态内部类In类的构造器必须使用外部类对象来调用,代码中super代表调用In类的构造器,而out代表外部类对象.

如果需要创建一个SubClass对象,就必须创建一个Out对象.因为SubClass是非静态内部类In的子类,非静态内部类In对象里必须有一个对Out对象的引用,其子类SubClass对象里也应该持有对Out对象的引用.当创建SubClass对象是传给该构造器的Out对象,就是SubClass里Out对象引用所指向的对象.

3.在外部类以外使用静态内部类

new OuterClass.InnerConstructor()
class StaticOut
{//  定义一个静态内部类,不使用访问控制符
//  即同一个包中的其他类可访问该内部类static class StaticIn{public StaticIn(){System.out.println("静态内部类的构造器。");}}
}
public class CreateStaticlnnerlnstance
{public static void main(String[] args){StaticOut.StaticIn in = new StaticOut.StaticIn();/** StaticOut.StaticIn in* in = new StaticOut.StaticIn();*/}
}

创建内部类对象时,静态内部类只需要外部类即可调用构造器,而非静态类必须使用外部类对象来调用构造器。

因为调用静态内部类的构造器时,无需使用外部类对象,所以创建静态内部类的子类也比较简单,下面代码就为静态内部类StaticIn类定义了一个空的子类。

public class StaticSubClass extends StaticOut.StaticIn{}

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

使用静态内部类比使用非静态内部类要简单,只要把外部类当成静态内部类的包空间即可,所以,当程序需要使用内部类时,应该优先考虑静态内部类。

内部类的类名不再是简单的内部类的类名构成,它实际上还把外部类的类名作为一个命名空间,作为内部类类名的限制。因此子类中的内部类和父类中的内部类不可能完全同名,即使二者所包含的内部类类名相同,但因为它们所处的外部类空间不同,所以它们不可能完全同名,也就不可能重写。

6.7.4 局部内部类

把一个内部类放在一个方法里定义,那么这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。

所有的局部变量都不能使用局部控制符。

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

public class LocallnnerClass
{public static void main(String[] args){//      定义局部内部类class InnerBase{int a;}
//      定义局部内部类子类class InnerSub extends InnerBase{int b;}
//      创建局部内部类的对象var is = new InnerSub();is.a = 5;is.b = 7;System.out.println("InnerSub对象的a和b实例变量是"+is.a+","+is.b);}
}

编译上面程序,会生成三个class文件LocallnnerClass$1InnerBase.class, LocallnnerClass1InnerSub.class,LocallnnerClass.class。这表明内部类class文件总是遵循如下命名规则OutClass1InnerSub.class, LocallnnerClass.class。这表明内部类class文件总是遵循如下命名规则OutClass1InnerSub.class,LocallnnerClass.class。这表明内部类class文件总是遵循如下命名规则OutClassNInnerClass.class。注意文件名比成员内部类的class文件多了有一个数字,这是因为同一个类里不可能有两个同名的成员内部类,而同一个类里则可能有两个以上同名的局部内部类(处于不同的方法中),所有增加了一个数字进行区分。

开发中很少用局部内部类

6.7.5 匿名内部类

匿名内部类适合创建那种只需要一次使用的类,例如前面介绍的命令模式所需要的Command对象,匿名内部类的语法有点奇怪,创建匿名内部类时会创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。

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

从上面定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口,还有两个规则如下:

  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许匿名内部类定义成抽象类。
  • 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。
interface Product
{double getPrice();String getName();
}
public class AnonymousTest
{public void test(Product p){System.out.println("购买了一个" + p.getName()+ "," + p.getPrice());}public static void main(String[] args) {var ta = new AnonymousTest();
//      调用test()方法时,需要传入一个Product参数
//      此处传入其匿名实现类的实例ta.test(new Product(){//匿名内部类类体部分public double getPrice() {return 567.8;}public String getName(){return "AGP显卡";}}//匿名内部类类体部分);/*匿名内部类类体部分可分解为class AnounymousProduct implements Product{public double getPrice() {return 567.8;}public String getName(){return "AGP显卡";}}ta.test(new AnounymousProduct());*/}
}

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

正如上面程序所看到的,定义匿名内部类无需class关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象。上面标注中为匿名内部类类体部分。

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

当通过实现接口来创建匿名内部类时,匿名内部类不能显式地定义构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值。

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

abstract class Device
{private String name;public abstract double getPrice();public Device() {}public Device(String name){this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}}
public class AnonymousInner
{public void test(Device d){System.out.println("购买了一个" + d.getName()+ "," + d.getPrice());}public static void main(String[] args) {var ai = new AnonymousInner();
//      调用有参数的构造器创建Device匿名实现类的对象ai.test(new Device("电子示波器"){public double getPrice() {return 67.8;}});
//      调用无参数的构造器创建Device匿名实现类的对象var d = new Device(){//                  初始化块{System.out.println("匿名内部类的初始化块。。。");}
//                  实现抽象方法public double getPrice(){return 56.2;}public String getName(){return "键盘";}};ai.test(d);}
}

创建了一个抽象父类Decice类,包含两个构造器,一个无参,一个有参,当以Device为父类的匿名内部类时,既可以传入参数(代表调用父类带参数的构造器),也可以不传入参数。(代表调用父类不带参数的构造器)。

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

如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final修饰。

interface A
{void test();
}
public class ATest
{public static void main(String[] args){int age = 8;/** 由于局部变量age被匿名内部类访问了,所以age将自动加上final修饰,所以不可修改age的值*/
//      age = 2;var a = new A(){public void test() {//                      从Java 8以前下面语句将提示错误:age必须使用final修饰
//                      从Java 8开始,匿名内部类、局部内部类允许访问非final的局部变量System.out.println(age);}};a.test();}
}

6.8 Java 11增强的Lambda表达式

Lambda支持将代码块作为方法参数,Lambda表达式允许更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。

6.8.1 Lambda表达式入门

下面使用匿名内部类来改写command表达式的例子。

public class CommandTest1
{public static void main(String[] args) {var pa = new ProcessArray();int[] target = {3,-4,6,4};
//      处理数组,具体处理行为取决于匿名内部类pa.process(target,new Command(){public void process(int element)  //粗体字开始{System.out.println("数组元素的平方是:" + element * element);} //粗体字结束});}
}

前面已近提到,processArray类的process()方法处理数组时,希望可以动态传入一段代码作为具体的处理行为,因此程序创建一个匿名内部类实例来封装处理行为。从上面代码可以看出,用于封装处理行为的关键就是实现程序中的粗体字方法。但是为了向process()方法传入这段粗体字代码,程序不得不使用匿名内部类的语法来创建对象。

Lambda表达式完全可以用于简化创建匿名内部类对象,如下

public class CommandTest3
{public static void main(String[] args){var pa = new ProcessArray();int[] array = {3,-4,6,4};
//      处理数组,具体处理行为取决于匿名内部类pa.process(array,(int element)->{System.out.println("数组元素的平方是:" + element * element);});}
}
(int element)->{System.out.println("数组元素的平方是:" + element * element);

这段代码中与创建匿名内部类时需要实现的process(int element)方法完全相同,只是不需要new Xxx(){}这种繁琐的代码,不需要指出重写的方法名字,也不需要给出重写的方法的返回值类型——只要给出重写的方法括号以及括号里的形参列表即可。

Lambda表达式的主要作用就是代替匿名内部类的繁琐语句。由

  • 形参列表:允许省略形参类型
  • 箭头(->)
  • 代码块:Lambda表达式需要返回值,而他的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。

下面程序师范了Lambda表达式的几种简化写法。

interface Eatable
{void taste();
}
interface Flyable
{String fly(String weather,String tool);
}
interface Addable
{int add(int a,int b);
}
public class LambdaQs
{//  调用该方法需要Eatable对象public void eat(Eatable a){System.out.println(a);a.taste();}public void drive(Flyable b){b.fly("【碧空如洗的晴天】","【直升机】");
//      System.out.println("我正在驾驶" + b);}public void test(Addable add){System.out.println("5和3的和为:" + add.add(5, 3));}public static void main(String[] args) {var lp = new LambdaQs();
//      lp.eat(()->System.out.println("苹果的味道很不错"));
        lp.drive((String weather,String tool)->{//          System.out.println("今天的天气是:" + weather);
//          System.out.println("直升机稳定飞行"+tool);
//      });
//      lp.test((a,b)->a+b);}
}

此段代码书中会因为两个变量未赋值而出现乱码,上面代码是自己修改后的,但是还是不会改,后期改进。

上面程序中的第一段粗体字代码使用Lambda表达式相当于不带形参的匿名方法由于该Lambda表达式的代码块只有一行代码,因此可以省略代码块的花括号;后两段同理。

第一段粗体字代码中调用了eat()方法,调用该方法需要一个Eatable类型的参数,但实际上传入的是Lambda表达式,后两段同理,但上面的程序可以正常变异,运行,这说明Lambda表达式实际上将会被当做“任意类型”的对象,到底需要当成何种类型的对象,这取决于运行环境的需要,下面详细介绍。

6.8.2 Lambda表打死与函数式接口

Lambda表达式的类型,也被称为“目标类型(targert type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。

如果采用匿名内部类语法来创建函数式接口实例,则只需要实现一个抽象方法,在这种情况下,可采用Lambda表达式来创建对象,该表达式创建出来的对象的目标类型就是这个函数式接口。

由于Lambda表达式的接口就是被当成对象,因此程序中完全客户以使用Lambda表达式进行赋值

public class LambdaTest {public static void main(String[] args) {//      Runnable 接口中只包含一个无参数的方法,
//      Lambda 表达式代表的匿名方法实现了Runnable 接口中唯一的、无参数的方法
//      因此下面的Lambda表达式创建了一个Runnable 对象Runnable r = () -> {for (var i = 0; i < 100; i++) {System.out.println();}};}
}

Runnable是Java本身提供的一个函数式接口。

从上面代码可以看出,Lambda实现的是匿名方法——因此他只能实现特定函数式接口中的唯一方法。这意味着Lambda表达式有如下两个特性:

  • Lambda表达式的目标类型必须是明确的函数式接口。
  • Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象。
Object obj = () -> {for(i=0;i<100;i++){}};

上面的代码与前一段代码几乎完全相同,只是此时程序将Lambda表达式不在赋值给Runnable变量,而是直接复制给Object变量。编译上面代码,会报错(不兼容的类型:Object不是函数接口)。

从该错误可以看出,Lambda表达式的目标类型必须是明确的函数式接口。尚敏代码将Lambda表达式赋给Object变量,编译器只能确定该Lambda表达式的类型为Object,而Object并不是函数式接口,因此会报错。

P220以后的Lambd内容暂时省略。(学不下去了)

6.9 枚举类

在某些情况下,一个类的对象是有限而且固定的,比如季节类,他只有4个对象;再比如行星类,目前只有8个对象。这种实例有限而且固定的类,在Java里被称为枚举类。

6.9.1 手动实现枚举类

在早期代码中,可能会直接使用简单的静态常量来表示枚举,例如以下代码:

public static final int SEASON_SPRING = 1;
public static final int SEASON_SUMMER = 2;
public static final int SEASON_FALL = 3;
public static final int SEASON_WINTER = 4;

6.9.2 枚举类入门

Java 5新增了一个 enum 关键字(他与class interface关键字的地位相同),用以定义枚举类,枚举类是一种特殊的类,他一样可以有自己的成员变量,方法,可以实现一个或多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,而且该Java源文件也必须与该枚举类的类名。

但枚举类终究不是普通类,他与普通类有如下区别

  • 枚举类可以实现一个或多个接口,使用enum定义的枚举默认继承了java.lang.Enum类,而不是默认继承了Object类,因此枚举类不能显式继承其他父类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。
  • 使用enum定义、非抽象的枚举类默认会使用final修饰。
  • 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。由于枚举类的所有构造器都是private的,而子类构造器总要调用父类一次,因此枚举类不能派生子类。
  • 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远不能派生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。

枚举类默认提供了一个values()方法,该方法可以很方便的遍历所有的枚举值。

public enum SeasonEnum {//在第一行列出4个枚举实例SPRING,SUMMER,FALL,WINTER;
}

这些枚举值代表了该枚举类的所有可能的实例。

如果需要使用该枚举类的某个实例,则可以使用EnumClass.variable的形式。如SeasonEnum.SPRING。

public class EnumTest
{public void judge(SeasonEnum s){//      switch语句里的表达式可以是枚举值switch(s){case SPRING:System.out.println("春暖花开。");break;case SUMMER:System.out.println("夏日炎炎。");break;case FALL:System.out.println("秋风瑟瑟。");break;case WINTER:System.out.println("冬日雪飘。");break;}}public static void main(String[] args){//      枚举类默认有一个values()方法,返回该枚举值的所有实例for(var s : SeasonEnum.values()){System.out.println(s);}
//      使用枚举实例时,可以通过EnumClass.variable形式来访问。new EnumTest().judge(SeasonEnum.SPRING);}
}

switch的控制表达式可以是任何枚举类型。而且case表达式中的值直接使用枚举值的名字,无须添加枚举类进行限定。

下面是java.lang.Enum类中所包含的方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C9m5AsKn-1616129487797)(C:\Program Files\Typora\image\0ee5804edadb22c1bba5195a057dc5a.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0TYEUpo-1616129487802)(C:\Program Files\Typora\image\825b1552f2d3b10d4e1cbe892dbd550.jpg)]

6.9.3 枚举类的成员变量、方法和构造器

枚举类也是一种类,只是它是一种比较特殊的类,因此他一样可以定义成员变量、方法和构造器。

public enum Gender
{MALE,FEMALE;  //成员变量
//  定义一个public修饰的实例变量public String name;
}
public class GenderTest
{public static void main(String[] args) {//      通过Enum的valueof()方法来获取指定枚举类的枚举值Gender g = Enum.valueOf(Gender.class, "FEMALE");
//      直接为枚举值的name实例变量赋值g.name = "女";
//      直接访问枚举值的name实例变量System.out.println(g + "代表:" + g.name);}
}

上面程序使用Gender枚举类时与使用一个普通类没有太大的差别,差别只是产生Gender对象的方式不同,枚举类的实例只能是枚举类,而不是随意地通过new来创建枚举类对象。

正如前面所提到的,Java应该把所有类设计成良好的封装的类,所以不应该允许直接访问Gender类的name成员变量,而是应该通过方法来控制对name的访问。否则可能出现很混乱的情形,例如上面程序恰好设置了g.name=“女”,要是采用g.name=“男”,那程序就会非常混乱了,可能出现FEMALE代表男的局面了,下面做出了改进:

public enum GenderBetter
{MALE,FEMALE;private String name;public void setName(String name){switch(this){case MALE:if(name.equals("男")){this.name = name;}else{System.out.println("参数错误");return;}break;case FEMALE:if(name.equals("女")){this.name = name;}else{System.out.println("参数错误");return;}break;}}public String getName(){return this.name;}
}

上面程序吧name设置成private,从而避免了其他程序直接访问该name成员变量,必须通过setName()方法来修改Getder实例的name变量,而setName()方法就可以保证不会产生混乱。上面程序中粗体字部分保证FEMALE枚举值的name变量只能设置成“女”,而MALE枚举值只能设置为男。

public class GenderBetterTest {public static void main(String[] args) {GenderBetter g = GenderBetter.valueOf("FEMALE");g.setName("女");System.out.println(g + "代表" + g.getName());
//      此时设置name值是将会提醒参数错误g.setName("男");System.out.println(g + "代表" + g.getName());}
}

上面的代码还不够好,枚举类通常应该设置成不可变类,也就是说,他的成员变量值不应该允许改变这样会更加安全,而且代码更加简洁。因此建议枚举类的成员变量使用public final修饰。

如果将所有的成员变量都使用fianl修饰符来修饰,所以必须在构造器里为这些成员变量指定初始值(或者在定义成员变量是指定默认值,或者在初始化块中指定初始值,但这两种情况不常见),因此应该为枚举类显式定义带参数的构造器。

public enum GenderBest {MALE("男"), FEMALE("女");  //粗体字
private final String name;
private GenderBest(String name)
{this.name = name;
}
private String getName()
{return this.name;
}
}

上面程序,为GenderBest枚举类构造一个GenderBest(String name)构造器之后,列出枚举值就应该采用粗体字来完成。也就是说。在枚举类中列出枚举值是,实际上就是调用构造器创建枚举类对象,只是这里无须使用new关键字,也无须显示调用构造器。前面列出枚举值时无须传入参数,升值无须使用括号,仅仅是因为前面的枚举值包含无参数的构造器。

//列出枚举值相当于
public static final GenderBest MALE = new GenderBest("男");
public static final GenderBest FEMALE = new GenderBest("女");

6.9.4 实现接口的枚举类

枚举类也能实现一个或多个接口。与普通方法实现一个或多个接口完全一样,枚举类实现一个或多个接口是,也需要实现该接口所包含的方法。

public interface GenderDesc {void info();
}

在上面GenderDesc接口中定义了一个Info()方法,下面的Gerder枚举类实现了接口,并实现了该接口里包含的info()方法。

public enum Gender1 implements GenderDesc
{MALE("男"), FEMALE("女");private final String name;private Gender1(String name){this.name = name;}private String getName(){return this.name;}public void info(){System.out.print("这是一个用于定义性别的枚举类");}
}

如果由枚举类来实现接口里的方法,则每个枚举类在调用该方法时都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方法,从而让不同的枚举值调用该方法时具有不用的行为方式。在下面的Gender枚举类中,不同的枚举值对info()方法的实现各不相同。

public enum Gender1 implements GenderDesc
{//  此处的枚举值必须调用对应的构造器来完成MALE("男")
//  花括号部分实际上是一个类体部分{public void info(){System.out.println("这个枚举类代表男性");}},FEMALE("女"){public void info(){System.out.println("这个枚举值代表女性");}};private final String name;public abstract void info();//自己加的,感觉加上这一句话更符合下面的解释private Gender1(String name){this.name = name;}private String getName(){return this.name;}//下面为自己加的测试public static void main(String[] args) {System.out.println(Gender1.FEMALE+"代表着"+Gender1.FEMALE.name);System.out.println(Gender1.MALE+"代表着"+Gender1.MALE.name);//System.out.println(Gender1.FEMALE.info());此行代码错误,学会后修改}
}

上面的代码的粗体字部分看起啦有些奇怪,当创建MALE和FEMALE两个枚举值时,后面又紧跟了一对花括号,这对花括号里包括了一个info()方法来定义。如果还记得匿名内部类的语法的话,则可能对这样的语法有点印象了,花括号部分实际上就是一个类体部分,这种情况下,当创建MALE、FEMALE枚举值时,并不是直接创建Gender枚举类的实例,而是相当于创建Gender的匿名子类的实例。因为粗体字括号部分实际上是匿名内部类的类体部分,所以这个部分的代码部分与前面介绍的匿名内部类语法大致相同,只是它依然是枚举类的匿名内部类的子类。

对于一个抽象的枚举类而言——只要它包含了抽象方法,他就是抽象枚举类,系统会默认用abstract修饰,而不是使用final修饰。

编译Gender1类即可发现出现三个class文件,即可证明MALE和FEMALE实际上是Gender1匿名子类的实例。

6.9.5 包含抽象方法的枚举类

定义一个抽象类的枚举类Opration ,让4个枚举值分别为eval()方法实现不同的加减乘除。

public enum Opration
{PIUS{public double eval(double x,double y){return x+y;}},MINUS{public double eval(double x,double y){return x-y;}},TIMES{public double eval(double x,double y){return x*y;}},DIVIDE{public double eval(double x,double y){return x/y;}};public abstract double eval(double x,double y);public static void main(String[] args) {System.out.println(Opration.PIUS.eval(5, 3));System.out.println(Opration.MINUS.eval(5, 3));System.out.println(Opration.TIMES.eval(5, 3));System.out.println(Opration.DIVIDE.eval(5, 3));}
}

编译此程序会出现5个class方法,Opration 对应一个,其他四个为四个匿名内部子类。

枚举类里定义抽象方法时不能使用abstract关键字来讲枚举类定义成抽象类(因为体统会自动为他添加abstract关键字),但因为枚举类需要显示创建枚举类,而不是父类,所以定义每个枚举值时必须为抽象方法提供实现,否则会编译错误。

6.10 对象与垃圾回收

Java的垃圾回收是Java语言的重要功能之一。当程序创建对象、数组等引用类型实体时,系统东辉在堆内存中为之分配一块内存,对象就保存在这块内存区中,当这块内存不再被任何引用类型引用时,这块内存就成了垃圾,等待垃圾回收机制进行回收。垃圾回收机制有如下机制:

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

6.10.1 对象在内存中的状态

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

  • 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。

  • 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,他就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态的finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。

  • 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有事该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态,系统才会真正回收该对象所占的资源。

public class StatusTranfer
{public static void test(){var a = new String("轻量级Java EE企业应用实战");//此段代码执行结束后“轻量级Java EE企业应用实战”字符串对象处于可达状态a = new String("疯狂Java讲义");//“轻量级Java EE企业应用实战”此时处于可恢复状态,而"疯狂Java讲义"处于可达状态}public static void main(String[] args) {test();}
}

一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或被其他对象的实例变量引用。当某个对象呗其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;当某个对象被其他对象的还是实例变量引用时,只有当该对象被销毁后,该对象才会进入可恢复状态。

6.10.2 强制垃圾回收

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

但任然可以强制系统进行垃圾回收——这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。强制系统垃圾回收方式有两种:

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

下面创建了4个匿名对象,每个对象创建之后立即进入可恢复状态,等待系统回收,但直到系统退出,系统依然不会回收该资源。

public class GcTest
{public static void main(String[] args){for(var i = 0;i < 4;i++){new GcTest();}}public void finalize(){System.out.println("系统正在清理GcTest对象的资源...");}
}

系统不曾调用GcTest对象的finalize()方法,修改如下:

public class GcTest
{public static void main(String[] args){for(var i = 0;i < 4;i++){new GcTest();
//          下面两行代码起的作用完全相同,强制系统进行垃圾回收
//          System.gc();Runtime.getRuntime().gc();}}public void finalize(){System.out.println("系统正在清理GcTest对象的资源...");}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZjE7GwoM-1616129487808)(C:\Program Files\Typora\image\04edad008bb713dd080adae680fdca9.png)]

运行java命令时指定-varbose:gc选项可以看待回收后的提示信息。

上面显示了程序强制垃圾回收的效果,但这种强制只是建议系统立即进行垃圾回收,系统完全有可能并不立即垃圾回收,垃圾回收机制也不会对程序的建议完全置之不理:垃圾回收机制会在收到通知后,尽快进行垃圾回收。

6.10.3 finalize方法

在垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源,在没有明确指定清理资源的情况下,Java提供了默认机制来清理该对象的资源,这个机制就是finalize()方法。该方法是定义在Object类里的实例方法,方法原型为:

protected void finalize() thows Throwable

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

记住以下几个特点:

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

下面程序演示了如何在finalize()方法中复活自身,并可通过该程序看出垃圾回收的不确定性

public class FinalizeTest
{public static FinalizeTest ft = null;   //定义了ft类变量public void info(){System.out.println("测试资源清理的finalize方法");}public static void main(String[] args){//      创建FinalizeTest对象立即进入可恢复状态new FinalizeTest();  //创建了一个 FinalizeTest类的匿名对象
//      通知系统进行资源回收System.gc();   //①
//      强制垃圾回收机制调用可恢复对象的finalize()方法
//      Runtime.getRuntime().runFinalization();   //②System.runFinalization();   //③ft.info();}public void finalize(){//      让ft引用到试图回收的可恢复对象,即可恢复对象重新变成可达ft = this;}
}

上面程序中定义了一个FinalizeTest类,重写了该类的finalize()方法,在该方法中把需要清理的可慧夫妇向重新赋值给ft引用变量,从而让该可恢复对象重新变成可达状态。

创建了一个 FinalizeTest类的匿名对象,但是没有给该对象赋值,所以立即进入可恢复状态。随后系统调用①通知系统进行垃圾回收,②强制系统立即调用可恢复对象的finalize()方法,再次调用ft对象的info()方法。info()任然可以正常运行。

删除①,取消强制垃圾回收后[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KfpPQXiN-1616129487811)(C:\Users\MingyangLiu\Desktop\张智超\image\Snipaste_2021-03-15_18-02-27.png)]

因为删除①后,程序并没有通知系统回收垃圾(因为内存并不紧张),也就意味着没有调用finalize(),也就是说ft类变量将依然保持为null,这样就导致了空指针异常。

②和③都用于强制回收垃圾机制调用可恢复对象的finalize()方法,如果程序仅执行System.gc();代码,而不执行②或③代码——由于JVM垃圾回收机制的不确定性,JVM往往不立即调用可恢复对象的finalize()方法,这样FinaizeTest的ft类变量可能依然为null,可能依然会导致空指针异常。

6.10.4 对象的软、弱和虚引用

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

1、强引用(StrongReference)

​ Java程序中最常见的引用方式。

2、软引用(SoftReference)

​ 通过SoftReference类实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收(看内存是否紧张),通常用于对内存敏感的程序中。

3、弱引用(WeakReference)

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

4、虚引用(PhantomReference)

​ 虚引用通过PhantomReference类实现,虚引用完全类型与没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。

引用队列由java.ref.ReferenceQueue类表示,它用于保存被回收后对象的引用。当联合使用软引用、弱引用和饮用队列时,系统在回收被引用的对象之后,将把被回收对象对应的引用添加到关联的引用队列中。虚引用在对象被释放之前,将把它对应的虚引用添加到关联的引用队列中,这使得可以在对象被回收前采取行动。

虚引用的主要作用就是用于跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用所引用的对象是否即将被回收。

import java.lang.ref.WeakReference;public class ReferenceTest
{public static void main(String[] args) {//      创建一个字符串对象var str = new String("疯狂Java讲义");
//      创建一个弱引用,让此弱引用引用到“疯狂Java讲义”字符串var wr = new WeakReference(str);  //①此处需要导包
//      切断str引用和“疯狂java讲义”字符串之间的引用联系str = null;    //②
//      取出弱引用所引用的对象
/*      .get():返回这个引用对象的引用对象。如果这个引用对象已经被程序或垃圾收集器清除,那么这个方法将返回null。返回:该引用所引用的对象,如果该引用对象已被清除,则返回null*/System.out.println(wr.get());   //③
//      强制垃圾回收System.gc();System.runFinalization();
//      再次取出弱引用所引用的对象System.out.println(wr.get());   //④}
}

上面:创建了一个疯狂Java讲义字符串对象,并让str引用变量引用。执行①,创建了一个弱引用对象,并让该对象与str引用同一对象。执行③,因为程序并不紧张,所以系统还不会回收wr所引用的对象,之后调用了System.gc()和System.runFinalize();通知系统进行垃圾回收,所以④输出null。

上面为弱引用,下面使用了虚引用来引用字符串对象,虚引用无法获取他引用的对象。下面还将虚引用与引用队列结合使用,可以看到虚引用所引用的对象被垃圾回收后,虚引用将被添加到引用队列中。

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;public class PhantomReferenceTest
{public static void main(String[] args) throws Exception{//      创建一个字符串对象var str = new String("疯狂Java讲义");
//      创建一个引用队列var rq = new ReferenceQueue();
//      创建一个虚引用,var pr = new PhantomReference(str,rq);
//      切断str引用str = null;
//      取出虚引用所引用的对象,并不能通过虚引用获取被引用的对象,所以输出nullSystem.out.println(pr.get());  //①
//      强制垃圾回收System.gc();System.runFinalization();
//      垃圾回收后,虚引用将被放入引用队列中
//      取出引用队列中最先进入队列的引用与pr进行比较System.out.println(rq.poll() == pr); //②}
}

因为系统无法通过虚引用来获得被引用的对象,所以执行①号粗体字代码时,程序将输出null(即使此时并未通过强制进行垃圾回收)。当程序强制垃圾回收后,只有虚引用引用的字符串对象将会被垃圾回收,当被引用的对象被回收后,对应的虚引用将被添加到关联的引用队列中,因而将在②看到输出turn。

必须要指出的是,要使用这些特殊的引用类,就不能保留对对象的强引用;如果保留了对对象的强引用,就会浪费这些引用类所提供的任何好处。

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

//取出弱引用所引用的对象
obj = wr.get();
//如果取出的对象为null
if(obj == null)
{//重新创建一个新的对象,再次让弱引用去引用该对象wr = new WeakReference(recreateIt());   //①//取出弱引用所引用的对象,将其赋给obj变量obj = wr.get();   //②
}
//操作obj对象
//再次切断obj与对象之间的联系
obj = null;
//取出弱引用所引用的对象
obj = wr.get();
//如果取出的对象为null
if(obj == null)
{//重新创建一个新的对象,并使用强引用来引用它obj = recreateIt(); //取出弱引用所引用的对象,将其赋给obj变量wr = new WeakReference(obj)
}
//操作obj对象
//再次切断obj与对象之间的联系
obj = null;

上面两段代码采用的都是为嘛,其中recreateIt()方法用于生成一个obj对象。这两段代码都是判断obj对象是否已经被回收,如果已经被回收,则重新创建该对象。如果弱引用引用的对象已经被垃圾回收释放了,则重新创建该对象。但第一段代码中存在一定的问题,当if块执行完成后,obj还是有可能为null。因为垃圾回收的不确定性,假设系统在①和②代码之间进行了垃圾回收,则系统会再次将wr所引用的对象回收,从而导师obj依然是null。第二段代码则不会存在这个问题,当if结束后,obj一定不为null。

6.11 修饰符的使用范围

外部类/接口 成员属性 方法 构造器 初始化块 成员内部类 局部成员
public 1 1 1 1 1
protected(受保护的) 1 1 1 1
包访问控制符 1 1 1 1 0 1 0
private(私人的) 1 1 1 1
abstract(抽象的) 1 1 1
final 1 1 1 1 1
static 1 1 1 1
strictfp 1 1 1
synchronized(同步的) 1
native 1
transient(短暂的) 1
volatile(不稳定的) 1
default 1

包访问控制符是一个特殊的修饰符,不用任何访问控制符的就是包访问控制符。对于初始化块和局部成员而言,他们不能使用任何访问控制符,所以看起来像包访问控制符。

strictfp关键字的含义是FP-strict,也就是精确浮点的意思。

native 关键字主要用于修饰一个方法,使用native修饰的方法类似于一个抽象方法。与抽象方法不同的是,native方法通常采用C语言来实现。如果某个方法需要利用平台的相关特性,或者访问系统引荐等,则可以使用native修饰该方法,再把该方法交给C去实现。

Java中的四个访问控制符权限从高到低分别为private、protected、包访问权限和private。四个访问控制符是互斥的,最多只能出现一个,不仅如初,abstract和final永远不能同时使用;abstract和static不能同时修饰方法,可以同时修饰内部类;abstract和private不能同时修饰方法,可以同时修饰内部类。由于private修饰的方法不可能被子类重写,因此使用final修饰没什么意义。

6.12 多版本JAR包

JAR文件的全称是Java Archive File,(是一种与平台无关的文件格式,可将许多文件聚合为一个文件。可以将多个Java小程序及其必需的组件(.class文件,图像和声音)捆绑到JAR文件中,然后通过一次HTTP事务将其下载到浏览器中,从而大大提高了下载速度。JAR格式还支持压缩,从而减小了文件大小,从而进一步缩短了下载时间。此外,小程序作者可以对JAR文件中的各个条目进行数字签名,以验证其来源。它是完全可扩展的。)>意思就是Java档案文件。通常JAR文件是一种压缩文件,与常见的ZIP压缩文件兼容,通常也被称为JRA包,在JAR文件中默认包含了一个名为META-INF/MANIFEST.MF的清单文件,这个清单文件是在生成JAR文件时由系统自动创建的。

《疯狂Java讲义》学习笔记 第六章 面向对象(下续)相关推荐

  1. 《疯狂Java讲义》学习笔记 第六章 面向对象(下)

    <疯狂Java讲义>学习笔记 第六章 面向对象(下) 6.1包装类 基本数据类型 包装类 byte Byte short Short int Integer long Long char ...

  2. 《疯狂的JAVA讲义》笔记-第8章集合

    <疯狂的JAVA讲义>笔记-第8章集合 Iterator .ListIterator接口 Iterator迭代时是将元素的值返回,并不是将元素本身返回,所以迭代时无法更改元素的值.但是可以 ...

  3. Unix原理与应用学习笔记----第六章 文件的基本属性2

    Unix原理与应用学习笔记----第六章 文件的基本属性2 改变文件权限命令:chmod 提示:文件或目录创建后,就被赋予一组默认的权限.所有的用户都有读,只有文件的所有者才有写. 相对权限设置 Ch ...

  4. 《Go语言圣经》学习笔记 第六章 方法

    <Go语言圣经>学习笔记 第六章 方法 目录 方法声明 基于指针对象的方法 通过嵌入结构体来扩展类型 方法值和方法表达式 示例:Bit数组 封装 注:学习<Go语言圣经>笔记, ...

  5. Java基础学习——第十六章 Java8新特性

    Java基础学习--第十六章 Java8 新特性 Java8(JDK8.0)较 JDK7.0 有很多变化或者说是优化,比如 interface 里可以有静态方法和默认方法,并且可以有方法体,这一点就颠 ...

  6. 疯狂Android讲义 - 学习笔记(二)

    疯狂Android讲义 - 学习笔记(二) Android应用的用户界面编程 2.1 界面编程与视图(View)组件 Android应用的绝大部分UI组件放在android.widget.androi ...

  7. 疯狂Kotlin讲义学习笔记04-05章:流程控制、数组和集合

    1.when分支取代swith分支 不在需要使用case关键字 case后面的冒号改为-> default改为更有意义的else 如果一个case里有多条语句,需要将多条语句用大括号括起来 wh ...

  8. 疯狂Kotlin讲义学习笔记07章:面向对象(上)对象,中缀,解构,幕后字段、属性,延迟初始化,访问控制符,构造器,继承,重写,super限定,重写,多态,is类型检查,as强制类型转换

    1.定义类的标准格式 修饰符 class 类名 [ constructor 主构造器]{零到多个次构造器定义零到多个属性....零到多个方法.... } 修饰符open是final的反义词,用于修饰一 ...

  9. 疯狂python讲义学习笔记——中十章完结

    #第十一章 thinker import tkinter as tk print(help(tk.Button.__init__))#以按扭为例查看有什么属性 class myApplication( ...

最新文章

  1. minheight能继承吗_借父母名买房到底归谁?其他兄妹能继承吗?
  2. Entity Framework 博客园专题
  3. CSP认证 201403-1相反数[C++题解]:哈希表
  4. Java并发编程(7):使用synchronized获取互斥锁的几点说明
  5. libmemcached安装报错
  6. ARKit从入门到精通(7)-ARCamera介绍
  7. Python工程师具备哪些技能才能提升求职机率?
  8. 几个 PHP 的“魔术常量”
  9. 软件项目组织管理(十)项目沟通管理
  10. 查询两个表合并成一个表
  11. Hadoop系列之Aggregate用法
  12. 杭电1466计算直线的交点数
  13. 关于%@ include file= %与jsp:include page=/jsp:include中的那些问题?
  14. 前后端分离式分布式微服务架构项目 学成在线开发项目 源码 视频 文档 工具 合集百度云下载地址
  15. Java 递归算法之斐波那契数列第 N 项
  16. MQTT X Web:在线的 MQTT 5.0 客户端工具
  17. 键盘按键发出声音,打不了字,提示启用筛选键
  18. mysql怎么设置id自动编号_MySQL中实现ID编号自动增加的方法
  19. 【阿里云】域名解析 Tomcat绑定域名
  20. Flume 海量日志收集利器

热门文章

  1. sql多表联查练习题
  2. 如何学好pathon
  3. esp32使用MicroPython驱动oled屏显示中文和英文
  4. 地理信息系统技术在地震应急中的应用
  5. java lamda表达式去重
  6. java 获取包路径_java获取java文件路径的四种方法
  7. 九宫格的布局你能够想到哪些办法实现?它们各自的优缺点是什么?
  8. cocos2dx之九宫格
  9. 添加主机并为物理网络适配器添加上行链路
  10. 压着谷歌打!ChatGPT提前上岗微软搜索,现在就能用,纳德拉:竞赛今天才开始...