第6章 - 面向对象(下)

面向对象(下)

1. 包装类

Java 为 8 种基本数据类型分别定义了相应的引用类型,使之可以当成 Object 类型变量使用,并称之为基本数据类型的包装类。

基本数据类型和包装类的对应关系:

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
charCharacter
floatFloat
doubleDouble
booleanBoolean
  • 自动装箱:可以把一个基本类型变量直接赋值给对应的包装类变量;
  • 自动拆箱:允许直接把包装类对象直接赋给一个对应的基本类型变量。
// 自动装箱
Integer inObj = 5;
// 自动拆箱
int it = inObj;

包装类可以实现基本类型变量和字符串之间的转换:

  • parseXxx(String s); (除 Character 之外所有包装类都提供了该方法)
  • valueOf(String s);
// 将字符串转换为基本类型
String intStr = "123";
int int1 = Integer.parseInt(intStr);
int int2 = Integer.valueOf(intStr);

String doubleStr = "4.56";
double double1 = Double.parseDouble(doubleStr);
double double2 = Double.valueOf(doubleStr);

String boolStr1 = "true";
String boolStr2 = "true1";
String boolStr3 = "false";
Boolean boolean1 = Boolean.parseBoolean(boolStr1);
Boolean boolean2 = Boolean.parseBoolean(boolStr2);
Boolean boolean3 = Boolean.parseBoolean(boolStr3);
System.out.println(boolean1);  // true
System.out.println(boolean2);  // false
System.out.println(boolean3);  // false

// 将基本类型转换为字符串
String str1 = String.valueOf(6.78f);
String str2 = String.valueOf('\u9999');
System.out.println(str1);  // 6.78
System.out.println(str2);  // 香

// 更加简单的写法
String str3 = 5 + "";
System.out.println(str3 == "5");  // true

比较值大小:

Integer in1 = 8;
Integer in2 = 8;
Integer ina = 128;
Integer inb = 128;
Integer inc = 500;
System.out.println(in1 == in2);  // true,Integer缓存了-128~127范围内的整数,可以直接判断
System.out.println(ina == inb);  // false
System.out.println(Integer.compare(ina, inb));  // 0
System.out.println(Integer.compare(ina, inc));  // -1
System.out.println(Integer.compare(inc, inb));  // 1

// 比较布尔值
System.out.println(Boolean.compare(true, false));  // 1
System.out.println(Boolean.compare(true, true));  // 0
System.out.println(Boolean.compare(false, true));  // -1

2. 处理对象

2.1 toString() 方法

toString() 方法是 Object 类里的一个实例方法,所有的 Java 类都是 Object 类的子类,因此所有的 Java 对象都具有 toString() 方法。

toString() 方法是一个“自我描述”方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述”信息,用以告诉外界该对象具有的状态信息。

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
}

public class PrintObject {
    public static void main(String[] args) {
        Person p = new Person("孙悟空");
        System.out.println(p);  // Person@368239c8
        System.out.println(p.toString());  // Person@368239c8
    }
}

重写 toString() 方法:

class Apple {
    private String color;
    private double weight;
    public Apple(String color, double weight) {
        this.color = color;
        this.weight = weight;
    }
    
    // 重写 toString() 方法
    public String toString() {
        return "Apple[color=" + color + ", weight=" + weight + "]";
    }
}

public class PrintObject {
    public static void main(String[] args) {
        Apple a = new Apple("红色", 5.68);
        System.out.println(a);  // Apple[color=红色, weight=5.68]
    }
}

2.2 == 和 equals 方法

  • ==:用于判断两个基本类型变量是否相等;两个字符串直接量是否相同;两个引用类型变量是否指向同一个对象;
  • equals:判断两个引用变量是否相等。
String str1 = "学习Java";
String str2 = "学习";
String str3 = "Java";
String str4 = "学习" + "Java";
String str5 = "学" + "习" + "Java";
String str6 = str2 + str3;
String str7 = new String("学习Java");
String str8 = new String("学习Java");

System.out.println(str1 == str4);  // true
System.out.println(str1 == str5);  // true
System.out.println(str1 == str6);  // false
System.out.println(str1 == str7);  // false
System.out.println(str7 == str8);  // false
System.out.println(str1.equals(str6));  // true
System.out.println(str1.equals(str7));  // true
System.out.println(str7.equals(str8));  // true

3. 类成员

static 关键字修饰的成员就是类成员。类成员属于整个类,不属于单个实例。

单例类

大部分时候都把类的构造器定义成 public 访问权限,允许任何类自由创建该类的对象。但有时候,不需要创建这么多对象。比如:系统可能只有一个窗口管理器、一个数据库引擎访问点。如果一个类始终只能创建一个实例,则这个类被称为单例类。

class Singleton {
    // 使用一个类变量来缓存曾经创建的实例
    private static Singleton instance;
    // 隐藏构造器
    private Singleton() {}
    
    // 提供一个静态方法,保证只产生一个 Singleton 对象
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

public class SingletonTest {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);  // true
    }
}

4. final 修饰符

final 关键字可用于修饰类、变量和方法,用于表示它修饰的类、方法和变量不可改变。

final 修饰变量时,表示该变量一旦获得了初始值就不可被改变。final 即可以修饰成员变量,也可以修饰局部变量、形参。

4.1 final 成员变量

final 修饰的成员变量必须由程序员显式地指定初始值

public class FinalVariableTest {
    final int a = 6;
    final String str;
    final int c;
    final static double d;

    // 初始化块,可对没有指定默认值的实例变量指定初始值
    {
        str = "Hello";
    }
    
    // 静态初始化块,可对没有指定默认值的类变量指定初始值
    static {
        d = 5.6;
    }

    // 构造器
    public FinalVariableTest() {
        c = 5;
    }

    public void changeFinal() {
        // 不能对 final 字段变量进行赋值
    }

    public static void main(String[] args) {
        var ft = new FinalVariableTest();
        System.out.println(ft.a);  // 6
        System.out.println(ft.c);  // 5
        System.out.println(ft.d);  // 5.6
    }
}

4.2 final 局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用 final 修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。

public void test(final int a) {
    // a = 5;  // 不能对 final 修饰的形参赋值
}

public static void main(String[] args) {
    final String str = "Hello";
    // str = "Java";  // str 已指定默认值,无法再赋值
    final int n;
    n = 6;
    // n = 7;  // final 修饰的变量,只能赋值一次
}

4.3 final 修饰基本类型变量和引用类型变量的区别

final 修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。

final int[] arr = {5, 2, 1, 4, 3};

Arrays.sort(arr);
System.out.println(Arrays.toString(arr));  // [1, 2, 3, 4, 5]

arr[2] = 6;
System.out.println(Arrays.toString(arr));  // [1, 2, 6, 4, 5]

// arr = null;  // 不能对 final 修饰的变量重新赋值

4.4 可执行“宏替换”的 final 变量

对一个 final 变量来说,不管它是类变量、实例变量,还是局部变量,只要该变量满足三个条件,这个 final 变量就不再是一个变量,而是相当于一个直接量。

  • 使用 final 修饰符修饰
  • 在定义该 final 变量时指定了初始值
  • 该初始值可以在编译时就被确定下来
public static void main(String[] args) {
    final int a = 5;
    System.out.println(a);
}

对于这个程序来说,变量 a 其实根本不存在,当程序执行 System.out.println(a); 代码时,实际转换为执行 System.out.println(5);

final 修饰符的一个重要用途就是定义“宏变量”。当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个 final 变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

如果被赋值的表达式只是基本的算术表达式或字符串连接运行,没有访问普通变量,调用方法,Java 编译器同样会将这种 final 变量当成“宏变量”处理。

4.5 final 方法

final 修饰的方法不可被重写,如果出于某些原因,不希望重写父类的某个方法,则可以使用 final 修饰该方法。

public class TestClass {
    public final void test() {}
    // 可以重载 final 方法
    public final void test(String msg) {}
}

class Sub extends TestClass {
    // public void test() {}  // 不能重写 final 方法
}

4.6 final 类

final 修饰的类不可以有子类。例如:java.lang.Math 类就是一个 final 类。

public final class FinalClass {}

4.7 不可变类

不可变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。Java 提供的 8 个包装类和 java.lang.String 类都是不可变类,当创建它们的实例后,其实例的实例变量不可改变。

class Name {
    private String firstName;
    private String lastName;
    
    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
    public String getLastName() {
        return lastName;
    }
}

public class Person {
    private final Name name;
    public Person(Name name) {
        // 设置 name 实例变量为临时创建的 Name 对象
        this.name = new Name(name.getFirstName(), name.getLastName());
    }
    
    public Name getName() {
        // 返回一个匿名对象
        return new Name(name.getFirstName(), name.getLastName());
    }

    public static void main(String[] args) {
        Name n = new Name("悟空", "孙");
        PersonTest p = new Person(n);

        System.out.println(p.getName().getFirstName());  // 悟空
        n.setFirstName("八戒");
        System.out.println(p.getName().getFirstName());  // 悟空
        
        System.out.println(n.getFirstName());  // 八戒
        PersonTest p2 = new PersonTest(n);
        System.out.println(p2.getName().getFirstName());  // 八戒
    }
}

上面程序可以看出,虽然改变对象 n 的成员变量值,但是保存在对象 p 里的成员变量 name 值并未改变。因此,如果引用类型的成员变量的类是可变的,就必须采取必要措施来保护该成员变量所引用的对象不会被修改,这样才能创建真正的不可变类。

5. 抽象类

有些时候,某个父类只知道其子类应该包含怎样的方法,但无法准确地知道这些子类如何实现这些方法。这时就可以使用抽象方法,抽象方法只有方法签名,没有方法实现的方法。

抽象方法和抽象类必须使用 abstract 修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。

  • 抽象类和抽象方法必须使用 abstract 修饰符来修饰,抽象方法不能有方法体;
  • 抽象类不能被实例化,无法使用 new 关键字来调用抽象类的构造器创建抽象类的实例;
  • 抽象类可以包含成员变量、方法、构造器、初始化块、内部类 5 种成分;
  • 含有抽象方法的类,只能被定义成抽象类。
// 抽象类 - 形状
public abstract class Shape {
    private String color;

    // 定义一个计算周长的抽象方法
    public abstract double calPerimeter();

    // 定义一个返回形状的抽象方法
    public abstract String getType();

	// 构造器
    public Shape() {}
    public Shape(String color) {
        this.color = color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getColor() {
        return this.color;
    }
}

// 类 - 三角形
public class Triangle extends Shape {
    // 定义三角形的三边
    private double a;
    private double b;
    private double c;

    public Triangle(String color, double a, double b, double c) {
        super(color);
        this.setSides(a, b, c);
    }

    public void setSides(double a, double b, double c) {
        if (a >= b + c || b >= a + c || c >= a + b) {
            System.out.println("三角形两边之和必须大于第三边");
            return;
        }
        this.a = a;
        this.b = b;
        this.c = c;
    }

    // 重写Shape类的的计算周长的抽象方法
    public double calPerimeter() {
        return a + b + c;
    }

    // 重写Shape类的的返回形状的抽象方法
    public String getType() {
        return getColor() + "三角形";
    }
}

// 类 - 圆
public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    // 重写Shape类的的计算周长的抽象方法
    public double calPerimeter() {
        return 2 * Math.PI * radius;
    }

    // 重写Shape类的的返回形状的抽象方法
    public String getType() {
        return getColor() + "圆形";
    }

    public static void main(String[] args) {
        Shape s1 = new Triangle("黑色", 3, 4, 5);
        Shape s2 = new Circle("黄色", 3);
        
        System.out.println(s1.getType());  // 黑色三角形
        System.out.println(s1.calPerimeter());  // 12.0
        System.out.println(s2.getType());  // 黄色圆形
        System.out.println(s2.calPerimeter());  // 18.84955592153876
    }
}

6. 接口

6.1 接口的概念和定义

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

[修饰符] interface 接口名 {
	常量定义
	抽象方法定义
	内部类、接口、枚举定义
	私有方法、默认方法、类方法定义
}
  • 修饰符可以是 public 或者省略,如果省略了 public 修饰符,则默认采用包权限访问控制符;
  • 接口名应与类名采用相同的命名规范;
  • 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类;
  • 接口中定义成员变量时,不管是否使用修饰符,成员变量总是总是使用 public static final 修饰符修饰;
  • 系统将自动为接口内普通方法增加 public abstract 修饰符,且不能有方法体。
public interface Output {
    // 接口里定义的成员变量只能是常量,系统会默认加上 public static final 修饰符
    int MAX_CACHE_LINE = 50;
    
    // 普通方法
    void out();
    void getData(String msg);
    
    // 默认方法
    default void print(String... msgs) {
        for (String msg : msgs) {
            System.out.println(msg);
        }
    }
    
    default void test() {
        System.out.println("默认的 test() 方法");
    }
    
    // 类方法
    static void staticTest() {
        System.out.println("类方法");
    }
    
    // 私有方法
    private void foo() {
        System.out.println("私有方法");
    }
    
    // 私有类方法
    private static void bar() {
        System.out.println("私有静态方法");
    }
}

6.2 接口的继承

[修饰符] interface 接口名 extends 父接口1, 父接口2... {
	...
}
interface InterfaceA {
    int PROP_A = 5;
    void testA();
}

interface InterfaceB {
    int PROP_B = 6;
    void testB();
}

interface InterfaceC extends InterfaceA, InterfaceB {
    int PROP_C = 7;
    void testC();
}

public class InterfaceTest {
    public static void main(String[] args) {
        System.out.println(InterfaceC.PROP_A);  // 5
        System.out.println(InterfaceC.PROP_B);  // 6
        System.out.println(InterfaceC.PROP_C);  // 7 
    }
}

6.3 使用接口

[修饰符] class 类名 implements 接口1, 接口2... {
	...
}
// 定义一个 Product 接口
interface Product {
    int getProduceTime();
}

// 让 Printer 类实现 Output 和 Product 接口
public class Printer implements Output, Product {
    private String[] printData = new String[MAX_CACHE_LINE];
    private int dataNum = 0;  // 记录当前打印数
    
    public void out() {
        // 只要还有作业,就继续打印
        while (dataNum > 0) {
            System.out.println("打印机打印:" + printData[0]);
            // 把作业队列整体前移一位,并将剩下的作业数减 1
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }
    
    public void getData(String msg) {
        if (dataNum >= MAX_CACHE_LINE) {
            System.out.println("输出队列已满,添加失败");
        } else {
            // 把打印数据添加到队列里,已保存数据的数理加 1
            printData[dataNum++] = msg;
        }
    }
    
    public int getProduceTime () {
        return 45;
    }

    public static void main(String[] args) {
        Output o = new Printer();
        o.getData("道可道,非常道;,名可名,非常名。");
        o.getData("无,名天地之始;有,名万物之母。");
        o.out();
        // --> 打印机打印:道可道,非常道;,名可名,非常名。
        // --> 打印机打印:无,名天地之始;有,名万物之母。
        
        o.getData("故常无,欲以观其妙;常有,欲以观其徼。");
        o.getData("此两者同出而异名,同谓之玄,玄之又玄,众妙之门。");
        o.out(); 
        // --> 打印机打印:故常无,欲以观其妙;常有,欲以观其徼。
        // --> 打印机打印:此两者同出而异名,同谓之玄,玄之又玄,众妙之门。
        
        o.print("孙悟空", "猪八戒", "白骨精");
        // --> 孙悟空
        // --> 猪八戒
        // --> 白骨精
        
        Product p = new Printer();
        System.out.println(p.getProduceTime());  // 45

    }

}

6.4 接口和抽象类

接口和抽象类都剧透如下特征:

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

从某种程度上来看,接口类似于整个系统的“总纲”,它制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。

抽象类不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。

6.5 面向接口编程

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

1. 简单工厂模式
// 定义 Computer 类
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();
    }
}

public class OutputFactory {
    // Output 工厂,负责生成 Output 对象
    public Output getOutput() {
        return new Printer();
    }

    public static void main(String[] args) {
        OutputFactory of = new OutputFactory();
        Computer c = new Computer(of.getOutput());
        
        c.keyIn("天下皆知美之为美,斯恶已;皆知善之为善,斯不善已。");
        c.keyIn("故:有无相生,难易相成,长短相较,高下相倾,音声相合,前后相随。");
        c.print();
        // --> 打印机打印:天下皆知美之为美,斯恶已;皆知善之为善,斯不善已。
        // --> 打印机打印:故:有无相生,难易相成,长短相较,高下相倾,音声相合,前后相随。
    }
}

如果我们有了更好的 BetterPrinter 需要替换旧的 Printer:

// 定义一个新的更好的 BetterPrinter 类
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]);
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }
    public void getData(String msg) {
        if (dataNum >= MAX_CACHE_LINE * 2) {
            System.out.println("输出队列已满,添加失败");
        } else {
            printData[dataNum++] = msg;
        }
    }
}

只需更改一步就可以了:

public class OutputFactory {
    // Output 工厂,负责生成 Output 对象
    public Output getOutput() {
        // return new Printer();  // 只要改变这里就可以了
        return new BetterPrinter();
    }

    public static void main(String[] args) {
        OutputFactory of = new OutputFactory();
        Computer c = new Computer(of.getOutput());
        
        c.keyIn("天下皆知美之为美,斯恶已;皆知善之为善,斯不善已。");
        c.keyIn("故:有无相生,难易相成,长短相较,高下相倾,音声相合,前后相随。");
        c.print();
        // --> 高速打印机正在打印:天下皆知美之为美,斯恶已;皆知善之为善,斯不善已。
        // --> 高速打印机正在打印:故:有无相生,难易相成,长短相较,高下相倾,音声相合,前后相随。
    }
}
2. 命令模式

先使用一个 Command 接口来定义一个方法,用这个方法来封装“处理行为”:

public interface Command {
    void process(int element);
}

分别定义不同的方法:

// 直接打印元素
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));
    }
}

命令模式的使用:

public class ProcessArray {
    // 只有当调用此方法时,才真正传入一个 Command 对象,才确定对数组的处理行为
    public void process(int[] target, Command cmd) {
        for (int t : target) {
            cmd.process(t);
        }
    }
}

public class CommandTest {
    public static void main(String[] args) {
        ProcessArray pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        
        pa.process(target, new PrintCommand());      
        System.out.println("=============");     
        pa.process(target, new SquareCommand());
    }
}

结果为:

迭代输出目标数组的元素:3
迭代输出目标数组的元素:-4
迭代输出目标数组的元素:6
迭代输出目标数组的元素:4
=============
数组元素的平方是:9
数组元素的平方是:16
数组元素的平方是:36
数组元素的平方是:16

7. 内部类

把一个类放在另一个类的内部定义,这个类被称为内部类,包含内部类的类也被称为外部类。

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

7.1 非静态内部类

public class Outer {
    private int prop = 9;
    private String outerStr = "外部 private 变量";
    
    class Inner {
        private int prop = 8;
        public void info() {
            System.out.println(prop);  // 8
            System.out.println(this.prop);  // 8
            System.out.println(Outer.this.prop);  // 9
            System.out.println(outerStr);  // 外部 private 变量
        }
    }
    
    static class StaticInner {
        private static int prop = 6;
        public void info() {
            System.out.println("static 内部类");
        }
    }
    
    public void test() {
        // 内部调用内部类
        System.out.println(new Inner().prop);  // 8
        System.out.println(StaticInner.prop);  // 6
    }

    public static void main(String[] args) {
        new Outer().test();
        // 在外部使用内部类
        Outer.Inner in1 = new Outer().new Inner();
        in1.info();
        // new Inner();  // 不能访问非静态内部类
        new StaticInner().info();  // static 内部类
        System.out.println(StaticInner.prop);  // 6
        
        // 在外部类以外使用静态内部类
        Outer.StaticInner in = new Outer.StaticInner();
        in.info();  // static 内部类
    }
}

非静态内部类里不能有静态方法、静态成员变量、静态初始化块。

7.2 静态内部类

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

7.3 使用内部类

  • 在外部类内部使用内部类:Inner in = new Inner();
  • 在外部类以外使用非静态内部类:Outer.Inner in = new Outer().new Inner();
  • 在外部类以外使用静态内部类:Outer.Inner in = new Outer.Inner();

7.4 局部内部类

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

7.5 匿名内部类

匿名内部类适合创建那些只需一次使用的类。创建匿名内部类时会创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。

  • 匿名内部类不能是抽象类
  • 匿名内部类不能定义构造器
interface Product1 {
    double getPrice();
    String getName();
}

public class AnonymousTest {
    public void test(Product1 p) {
        System.out.println("购买了一个" + p.getName() + "花掉了" + p.getPrice());
    }

    public static void main(String[] args) {
        // 匿名内部类访问的局部变量,必须使用 final 修饰。
        // Java 8 以后的版本可以省略 final 不写,但必须按照有 final 修饰的方式来用 —— 即一次赋值后,不能再重新赋值。
        String name = "老王";
        AnonymousTest ta = new AnonymousTest();
        
        // 使用匿名内部类实例
        ta.test(new Product1() {
            {
                System.out.println("土豪" + name);
            }

            public double getPrice() {
                return 12999.0;
            }
            public String getName() {
                return "华硕 RTX-2080Ti ";
            }
        });
    }
}
// 输出结果:
// --> 土豪老王
// --> 购买了一个华硕 RTX-2080Ti 花掉了12999.0

8. Lambda 表达式

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

// 方式一:
(形参列表) -> {
	// 代码块;
}

// 只有一个形参,简写方式:
形参 -> {
	// 代码块;
}

// 没有形参,简写方式:
() -> {
	// 代码块;
}

// 只有一条语句,简写方式:
(a, b) -> a = b

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

public class LambdaTest {
    public static void main(String[] args) {
        ProcessArray pa = new ProcessArray();
        int[] array = {3, -4, 6, 4};
        // 使用匿名内部类
        pa.process(array, new Command() {
            @Override
            public void process(int e) {
                System.out.println("数组元素 " + e + " 的平方是:" + e * e);
            }
        });

        // 使用 Lambda 表达式,更加简洁明了
        System.out.println("====================");
        pa.process(array, (int e) -> {
            System.out.println("数组元素 " + e + " 的平方是:" + e * e);
        });
    }
}

Lambda 表达式的两个限制:

  • Lambda 表达式的目标类型必须是明确的函数式接口;
  • Lambda 表达式只能为函数式接口创建对象。Lambda 表达式只能实现一个方法,因此只能为只有一个抽象方法的接口(函数式接口)创建对象。

Lambda 表达式对方法和构造器的引用:

import java.util.Arrays;

@FunctionalInterface
interface Command {
    void process(int element);
}

@FunctionalInterface
interface Converter {
    Integer convert(String from);
}

@FunctionalInterface
interface MyTest {
    String test(String str, int a, int b);
}

@FunctionalInterface
interface MyTest2 {
    PersonTest setName(String name);
}

class PersonTest {
    private String name;

    public PersonTest(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

public class LambdaTest {
    public static void main(String[] args) {
        // 1.引用类方法,函数式接口中被实现方法的全部参数传递给该类方法作为参数
        Converter c1 = Integer::valueOf;
        System.out.println(c1.convert("129"));  // 129

        // 2.引用特定对象的实例方法,函数式接口中被实现方法的全部参数传递给该方法作为参数
        Converter c2 = "君不见黄河之水天上来"::indexOf;
        System.out.println(c2.convert("黄"));  // 3

        // 3.引用某类对象的实例方法,第一个参数作为调用者,后面的参数全部传递给该方法作为参数
        MyTest mt = String::substring;
        System.out.println(mt.test("君不见黄河之水天上来", 3, 7));  // 黄河之水

        // 4.引用构造器,函数式接口中被实现方法的全部参数传递给该构造器作为参数
        MyTest2 mt2 = PersonTest::new;
        mt2.setName("老王");

        // 使用 Lambda 表达式调用 Arrays 的类方法
        String[] arr1 = {"埃斯库罗斯", "白居易", "仓央嘉措", "李白"};
        Arrays.parallelSort(arr1, (a, b) -> a.length() - b.length());
        System.out.println(Arrays.toString(arr1));  // [李白, 白居易, 仓央嘉措, 埃斯库罗斯]

        int[] arr2 = {3, -4, 25, 16, 30, 18};
        Arrays.parallelPrefix(arr2, (l, r) -> l + r);
        System.out.println(Arrays.toString(arr2));  // [3, -1, 24, 40, 70, 88]

        int[] arr3 = new int[5];
        Arrays.parallelSetAll(arr3, index -> index * 5);
        System.out.println(Arrays.toString(arr3));  // [0, 5, 10, 15, 20]
    }
}

9. 枚举类

在某些情况下,一个类的对象是有限且固定的,比如季节类,它只有4个对象。这种实例有限且固定的类,在 Java 里被称为枚举类。

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER;
}

枚举类与普通类的区别:

  • 枚举类可以实现一个或多个接口,使用 enum 定义的枚举类默认继承了 java.lang.Enum 类,而不是默认继承 Object 类,因此枚举类不能显式继承其他父类;
  • 使用 enum 定义、非抽象的枚举类默认会使用 final 修饰;
  • 枚举类的构造器只能使用 private 访问控制符,如果省略了构造器的访问控制符,则默认使用 private 修饰;如果强制指定访问控制符,则只能指定 private 修饰符。由于枚举类的所有构造器都是 private 的,而子类构造器总要调用父类构造器一次,因此枚举类不能派生子类;
  • 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加 public static final 修饰,无须程序员显式添加。
public class EnumTest {
    private void judge(Season s) {
        switch (s) {
            case SPRING:
                System.out.println("春天到了,播种的季节");
                break;
            case SUMMER:
                System.out.println("夏天到了,游泳的季节");
                break;
            case AUTUMN:
                System.out.println("秋天到了,收获的季节");
                break;
            case WINTER:
                System.out.println("冬天到了,下雪的季节");
                break;
            default:
                System.out.println("你已跳出四季轮回,祝你渡劫成功!");
        }
    }

    public static void main(String[] args) {
        // 使用枚举类默认的 values() 方法,返回该枚举类的所有实例
        for (Season s : Season.values()) {
            System.out.println(s);
        }

        // 使用枚举类实例
        new EnumTest().judge(Season.AUTUMN);  // 秋天到了,收获的季节
    }
}

10. 修饰符的适用范围

外部类/接口成员属性方法构造器初始化块成员内部类局部成员
public
protected
包访问控制符
private
abstract
final
static
strictfp
synchronized
native
transient
volatile
default
  • strictfp 关键字含义是 FP-strict,也就是精确浮点的意思。使用了此修饰符的类、接口、方法,可以在进行浮点运算时更加精确。
  • native 关键字修饰的方法类似于一个抽象方法,通常采用 C 语言来实现。如果某个方法需要利用平台相关特性,或者访问系统硬件等,则可以使用 native 修饰该方法,再把该方法交给 C 去实现。一旦 Java 程序中包含了 native 方法,这个程序将失去跨平台的功能。
  • 4 个访问控制符是互斥的,最多只能出现其中之一。
  • abstract 和 final 永远不能同时使用。
  • abstract 和 static 不能同时修饰方法,可以同时修饰内部类。
  • abstract 和 private 不能同时修饰方法,可以同时修饰内部类。
  • private 和 final 同时修饰方法虽然语法是合法的,但没有太大意义(private 修饰的方法不可能被子类重写,因此使用 final 修饰没有意义)

11. 练习

  • 通过抽象类定义车类的模板,然后通过抽象的车类来派生拖拉机、卡车、小轿车。
  • 定义一个接口,并使用匿名内部类方式创建接口的实例。
  • 定义一个函数式接口,并使用 Lambda 表达式创建函数式接口的实例。
  • 定义一个类,该类用于封装一桌梭哈游戏,这个类应该包含桌上剩下的牌的信息,并包含 5 个玩家的状态信息:他们各自的位置、游戏状态(正在游戏或已放弃)、手上已有的牌等信息。如果有可能,这个类还应该实现发牌方法,这个方法需要控制从谁开始发牌,不要发牌给放弃的人,并修改桌上剩下的牌。
  • 将学到的知识和遇到的问题,整理成笔记,记录下来
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值