《Java语言及网络编程》课后小结

What’s Java?

Java的设计哲学

每一种语言都有它的设计哲学,如python的准则——著名的“python之禅”。

那么我们所熟知的C++呢?C++之父在《C++语言的设计和演化》一书中写出了C++的目标与一般性规则(Chap4 C++语言设计规则)。

image-20211219213320557

可以看到,这似乎与python是相反的设计理念——python希望开发者“用一种方法,最好是只有一种方法来做一件事”,“Beautiful is better than ugly.”,而C++希望“总提供一条转变的通路”、“为每种应该支持的风格提供全面支持”。

私以为C++的设计理念导致了它如今的些许落寞——很多人认为C++是他们痛苦记忆的根源,连《Java核心技术》中都写道

但不可否认,现代C++是难入门的、复杂的、效率高的、熟悉后很实用的一门语言。

那么Java呢?它的设计理念是什么?

Java的设计者编写了一本颇有影响力的“白皮书”用于解释设计的初衷及完成的情况。

它的摘要中写道

1.2.1 简单、面向对象、熟悉

1.2.2 健壮和安全

1.2.3 架构中立且可移植

1.2.4 高性能

1.2.5 解释型、线程型和动态型

拆开来就是

另外,这本白皮书每章前面都有一些名言,我很喜欢。eg.

这是一些具体的设计原则,更加抽象的如下

因为课程还没有上完,所以我对它的理解是:类似C++的、全面向对象的、支持网络编程的、比C++简单且安全的一门语言。

Java的各种专业名词

老师上课总说的Java SE 5是什么?JDK1.5又是什么?为什么配套的东西会有两个版本号?

详见《Java核心编程》Chap2

Java SE版本 JDK版本 发布时间 开发代号
Oak 1995-05-23 Oak(橡树)
Java 1.0 JDK1.0 1996-01-23
Java 1.1 JDK1.1 1997-02-18
J2SE 1.2 JDK1.2 1998-12-04 Playground(运动场)
J2SE 1.3 JDK1.3 2000-05-08 Kestrel(美洲红隼)
J2SE 1.4 JDK1.4 2002-02-13 Merlin(灰背隼)
Java SE 5.0 JDK1.5 2004-09-29 Tiger(老虎)
Java SE 6 JDK1.6 2006-12-11 Mustang(野马)
Java SE 7 JDK1.7 2011-07-28 Dolphin(海豚)
Java SE 8 JDK1.8 2014-03-18 Spider(蜘蛛)
Java SE 9 JDK1.9 2017-09-21
Java SE 10 JDK10 2018-03-21
Java SE 11 JDK11 2018-09-25
Java SE 12 JDK12 2019-3-20

另外,相比于“懒惰”的C++,“勤奋”的Java的更新频率要高得多。(C++语言近几次更新的标准是C++20、C++17、C++14、C++11、C++03、C++98,要么很久不更新,要么一更新就带来一大堆全新特性,近几年因感受到了威胁,C++委员会提高了更新频率)。

11.29 基本语法

  1. 发现java17中的java <file>.java似乎只能编译单个类

    另外,想要从指令路径中运行class,需要使用java -classpath <dir> <file>命令

    所以vscode中java对应的task.json可以这样配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    {
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
    {
    "label": "javac",
    "command": "javac ${file} -d ${fileDirname}\\bin",
    // "command": "javac ${file}",
    "type": "shell",
    "group": {
    "kind": "build",
    "isDefault": true
    },
    "presentation": {
    "reveal": "always",
    "panel": "new",
    "focus": true
    }
    },
    {
    "label": "run",
    "command": "java -classpath ${fileDirname}\\bin ${fileBasenameNoExtension}",
    "type": "shell",
    "dependsOn": "javac",// 加入这个依赖就会先javac生成class文件
    "group": {
    "kind": "test",
    "isDefault": true
    },
    "presentation": {
    "reveal": "always",
    "panel": "new",
    "focus": true
    }
    }
    ]
    }
  2. java先把所有的类加载到正文段,同时静态初始化(static执行的时机),类似于预编译

  3. Math库中都是静态方法,都不需要生成对象(eg. random),静态方法可以通过import引入

  4. 单例设计模式:创建型模式,提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

    • 单例类只能有一个实例。
    • 单例类必须自己创建自己的唯一实例。
    • 单例类必须给所有其他对象提供这一实例。

    单例模式事实上有6种,课上介绍的这种叫做“线程安全的懒汉式”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 线程安全的懒汉式
    class T {
    private T() {
    }

    private static T obj;

    public static T getInstance() {
    if (obj == null)
    obj = new T();
    return obj;
    }
    }

    public class signle {
    public static void main(String[] args) {
    T t1 = T.getInstance();
    T t2 = T.getInstance();
    System.out.println(t1 == t2);
    }
    }

    这体现出两次获取的都是同一对象

  5. 关于==和equals

    == 对于基本类型来是值比较,对于引用类型是引用比较;

    equals默认情况下是引用比较,很多类重写了 equals 方法把它变成了值比较,如 String、Integer。

    所以一般情况下 equals 比较的是值是否相等。

  6. 父类引用不能被强制转换为子类引用,子类引用可以隐式转换/强制转换成父类引用(指针)

    子类引用按需隐式转换到父类引用

  7. switch可以接收字符串(包括空串””),不能接收null,因为java的switch实现基于hashCode,null的hashCode值为0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class switchTest {
    public static void main(String[] args) {
    String s=null; // NullPointerException
    // String s = "";

    // if (s != null) // 若禁止传递空指针时的一种解决方法
    // switch (s) {
    switch (s == null ? "null" : s) { // 若必须检测空指针时的一种解决方法
    // //null in switch cases 是预览功能,默认情况下禁用
    // case null :
    // System.out.println("This is a nullptr?");
    // break;
    case "null":
    System.out.println("This is a \"null\"!");
    break;
    case "":
    System.out.println("This is a blank string!");
    break;
    default:
    System.out.println("This is a other string!");
    }
    }
    }
  8. instanceof,二元操作符,使用方式类似于==

    作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。

  9. 实例和引用是两回事!一个实例如果不是被本类的引用所调用,可能无法调用实例的方法

    例如父类引用调用子类实例便无法使用子类的方法

    使用instanceof将实例和其本类的引用配套

  10. java11后的instanceof可以在判断为true后直接赋给本类引用,引用名为最后的参数

    这可能是个语法糖,因为它不是三元运算符

12.1 继承、多态

  1. 编译时类型和运行时类型

    Java中的许多对象(一般都是具有父子类关系的父类对象)在运行时都会出现两种类型:编译时类型和运行时类型。

    例如:Person person = new Student();这行代码将会生成一个person变量,该变量的编译时类型是Person,运行时类型是Student。

    编译时类型由声明该变量时使用的类型(引用的类型)决定,运行时类型由实际赋给该变量的对象的类型(实例的类型)决定。

    变量在编译阶段只能调用其编译时类型所具有的方法,在运行阶段执行它运行时类型所具有的方法。即变量只能引用所在的类里的方法。

    另外通过引用来访问对象包含的实例属性时,系统总是试图访问它编译时类所定义的属性,而不是它运行时所定义的属性。

    因为有编译时类型的限制,所以变量所调用的方法只能来自编译时类型的方法

    举两个例子:访问方法和访问属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class Father {
    public void Show(Father obj) {
    System.out.println("in Father class, receive Father obj.");
    }
    }

    class Son extends Father {
    public void Show(Father obj) {
    System.out.println("in Son class, receive Father obj.");
    }

    public void Show(Son obj) {
    System.out.println("in Son class, receive Son obj.");
    }

    public void Show(GrandSon obj) {
    System.out.println("in Son class, receive GrandSon obj.");
    }
    }

    class GrandSon extends Son {

    }

    public class Inheritance1 {
    public static void main(String[] args) {
    Father father = new Son();
    GrandSon grandSon = new GrandSon();
    father.Show(grandSon);
    }
    }

    继承链(inheritance chain):

网上找的一份第二次作业第一题的答案,不小心粘在这了

①②③比较好理解,一般不会出错。④⑤就有点糊涂了,为什么输出的不是”B and B”呢?!!先来回顾一下多态性。

运行时多态性是面向对象程序设计代码重用的一个最强大机制,动态性的概念也可以被说成“一个接口,多个方法”。Java实现运行时多态性的基础是动态方法调度,它是一种在运行时而不是在编译期调用重载方法的机制。 方法的重写Overriding和重载Overloading是Java多态性的不同表现。重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写(Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloaded的方法是可以改变返回值的类型。

当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。 (但是如果强制把超类转换成子类的话,就可以调用子类中新添加而超类没有的方法了。)

好了,先温习到这里,言归正传!实际上这里涉及方法调用的优先问题 ,优先级由高到低依次为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。让我们来看看它是怎么工作的。

比如④,a2.show(b),a2是一个引用变量,类型为A,则this为a2,b是B的一个实例,于是它到类A里面找show(B obj)方法,没有找到,于是到A的super(超类)找,而A没有超类,因此转到第三优先级this.show((super)O),this仍然是a2,这里O为B,(super)O即(super)B即A,因此它到类A里面找show(A obj)的方法,类A有这个方法,但是由于a2引用的是类B的一个对象,B覆盖了A的show(A obj)方法,因此最终锁定到类B的show(A obj),输出为”B and A”。 再比如⑧,b.show(c),b是一个引用变量,类型为B,则this为b,c是C的一个实例,于是它到类B找show(C obj)方法,没有找到,转而到B的超类A里面找,A里面也没有,因此也转到第三优先级this.show((super)O),this为b,O为C,(super)O即(super)C即B,因此它到B里面找show(B obj)方法,找到了,由于b引用的是类B的一个对象,因此直接锁定到类B的show(B obj),输出为”B and B”。 按照上面的方法,可以正确得到其他的结果。

12.8 接口、lambda表达式、内部类

接口

接口是对希望符合这个接口的类的一组需求

接口没有实例。

接口中的所有方法都是public,所有字段都是public static final

注:Java语言规范建议不要提供多余的关键字

实现接口的类若非抽象类,则必须实现接口中的所有方法

实现接口时必须将方法声明为public,因为缺省访问权限为包内权限,编译器会报错,认为你试图提供更严格的(更弱的)访问权限

测试用到的代码

类图:

代码:

  1. Person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package edu.wanpengxu.course.interfacetest;

public class Person {
String name;

public Person(String n) { // Java不支持默认值参数
name = n;
}

public Person() { // 但可以使用重载实现类似的功能
this("路人甲");
}

public void eat() { // getMethods()只能获取父类中public权限的方法!如果不加public会出现虽然能调用但是反射获取不到的情况。
System.out.println("The person is eating.");
}
}
  1. CanFight.java

    1
    2
    3
    4
    5
    6
    7
    package edu.wanpengxu.course.interfacetest;

    public interface CanFight {
    int numberOfWins = 10;

    void fight();
    }
  2. CanSwim.java

    1
    2
    3
    4
    5
    package edu.wanpengxu.course.interfacetest;

    public interface CanSwim {
    void swim();
    }
  3. Hero.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package edu.wanpengxu.course.interfacetest;

    public class Hero extends Person implements CanFight, CanSwim {
    String title;

    public Hero(String n, String t) {
    super(n);
    title = t;
    }

    public Hero() {
    super();
    title = "无证骑士";
    }

    @Override
    public void fight() {
    System.out.println("The hero is fighting.");
    }

    @Override
    public void swim() {
    System.out.println("The hero is swimming.");
    }
    }
  4. Test.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    package edu.wanpengxu.course.interfacetest;

    import java.lang.reflect.Method;

    public class Test {
    public static void main(String[] args) {
    Person h1 = new Hero();
    Hero h2 = new Hero("琦玉", "秃头披风侠");
    System.out.println("Hero is a subtype of Person? " + (h2 instanceof Person));
    System.out.println("Hero is a subtype of CanSwim? " + (h2 instanceof CanSwim));
    System.out.println("Hero is a subtype of CanFight? " + (h2 instanceof CanFight));

    System.out.println();
    Class<? extends Person> h1RuntimeClass = h1.getClass();
    System.out.println(h1RuntimeClass);
    Method[] methods = h1RuntimeClass.getMethods();
    for (Method method : methods)
    System.out.println(method);

    System.out.println();
    Class<? extends Person> h2RuntimeClass = h2.getClass();
    System.out.println(h2RuntimeClass);
    Method[] methods2 = h2RuntimeClass.getMethods();
    for (var method : methods2)
    System.out.println(method);

    System.out.println();
    System.out.println(h1.name);
    // System.out.println(h1.title); // ERROR
    h1.eat();
    // h1.fight(); // ERROR
    // h1.swim(); // ERROR

    System.out.println();
    System.out.println(h2.name);
    System.out.println(h2.title);
    h2.eat();
    h2.fight();
    h2.swim();
    }
    }
  5. output.out

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    Hero is a subtype of Person? true
    Hero is a subtype of CanSwim? true
    Hero is a subtype of CanFight? true

    class edu.wanpengxu.course.Hero
    public void edu.wanpengxu.course.Hero.fight()
    public void edu.wanpengxu.course.Hero.swim()
    public void edu.wanpengxu.course.Person.eat()
    public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
    public final void java.lang.Object.wait() throws java.lang.InterruptedException
    public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
    public boolean java.lang.Object.equals(java.lang.Object)
    public java.lang.String java.lang.Object.toString()
    public native int java.lang.Object.hashCode()
    public final native java.lang.Class java.lang.Object.getClass()
    public final native void java.lang.Object.notify()
    public final native void java.lang.Object.notifyAll()

    class edu.wanpengxu.course.Hero
    public void edu.wanpengxu.course.Hero.fight()
    public void edu.wanpengxu.course.Hero.swim()
    public void edu.wanpengxu.course.Person.eat()
    public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
    public final void java.lang.Object.wait() throws java.lang.InterruptedException
    public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
    public boolean java.lang.Object.equals(java.lang.Object)
    public java.lang.String java.lang.Object.toString()
    public native int java.lang.Object.hashCode()
    public final native java.lang.Class java.lang.Object.getClass()
    public final native void java.lang.Object.notify()
    public final native void java.lang.Object.notifyAll()

    路人甲
    The person is eating.

    琦玉
    秃头披风侠
    The person is eating.
    The hero is fighting.
    The hero is swimming.

反射只能获取public方法

接口的属性

可以使用接口引用变量,变量必须是实现了接口的类。

1
2
Comparable x;
x = new Employee(...);

可以使用instanceof检查一个对象是否实现了某个特定的接口

1
2
3
if (anObject instanceof Comparable) {
...
}

允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口

1
2
3
4
5
6
public interface Moveable {
void move (double x, double y);
}
public interface Powered extends Moveable {
double milesPerGallon();
}

一个类可以实现多个接口

1
class Employee implements Cloneable, Comparable

接口与抽象类

为什么Java中会有接口?

因为,有些程序设计语言(尤其是C++)允许一个类有多个超类,这个特性称为多重继承(multiple inheritance),Java的设计者选择了不支持多重继承,其主要原因是多重继承会让语言变得非常复杂(如同C++),或者效率会降低(如同Eiffel)。

接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。

所以Java只能扩展一个类却可以实现多个接口

1
2
class Employee extends Person, Comparable  // ERROR
class Employee extends Person implements Comparable // OK

默认方法

可以使用default修饰符为接口方法提供一个默认实现,这个默认方法主要用于“接口演化”,例如很久以前有这样一个类

1
public class Bag implements Collention

后来Java要在Collention中增加一个stream方法,如果这个方法不是默认方法,由于Bag此前并没有实现stream方法,程序将不能编译,即不能保证“源代码兼容”

“二进制兼容”:在升级库文件的时候,不必重新编译使用此库的可执行文件或其他库文件,并且程序的功能不被破坏。

“源代码兼容”:在升级库文件的时候,不必修改使用此库的可执行文件或其他库文件的源代码,只需重新编译应用程序,即可使程序的功能不被破坏。

增加非默认方法stream后,若不对Bag类重新编译(即使用之前编译好的JAR文件),当程序在Bag实例上调用steam方法时会出现AbstractMethodError

若增加的是默认方法,那么上述情况会调用Collection.stream方法

解决默认方法冲突

  1. 超类优先

    若一个类继承了超类,实现了接口,且超类和接口中有相同签名的方法,使用超类中的方法

  2. 接口冲突(让程序员解决)

    使用下面的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    interface Person {
    default String getName() {
    return "" ;
    }
    }
    interface Named{
    default String getName() {
    return getClass().getName() + "_" + hashCode();
    }
    }
    class Student implements Person, Named{ // ERROR
    ...
    }

    Java编译器会报错,让程序员来解决这个问题,解决方法是选择冲突方法中的一个

    1
    2
    3
    4
    5
    class Student implements Person, Named {
    public String getName() {
    return Person.super.getName();
    }
    }

    使用{接口名}.super.{方法名}的原因:在类继承中,我们使用super.{方法名}便可以调用超类的方法,但接口允许多继承,所以必须在super前指定是哪个接口。

    只要被实现的接口中的方法有一个提供了默认实现,编译器就会报错,若方法都没有默认实现则不存在冲突,因为必须在实现这些接口的类中实现方法。

lambda表达式

lambda表达式:匿名函数,是代码块以及必须传入代码块的变量规范。

为什么是λ?

这是逻辑学家Alonzo Church受《数学原理》(Principia Mathematica)中用重音符^表示自由变量启发,采用大写lambda(Λ)表示参数,后来又改为了小写lambda(λ)。从此带参数变量的表达式就被称为lambda表达式。

语法

语法格式为

1
(parameters) -> expression


1
2
3
(parameters) -> {
statements;
}

如:

1
2
3
(String first, String second) -> {
first.length()-second.length();
}

特征:

  1. 可选参数类型:由编译器识别参数类型,可以忽略参数类型
  2. 可选的圆括号:若只有一个参数,可以忽略圆括号
  3. 可选的大括号及return:若只有一条语句,可以忽略大括号、分号及return

若一个lambda表达式只在某些分支有返回值而其他分支无返回值,编译器会报错,如

1
2
3
4
(int x) -> {
if (x >= 0)
return 1;
} // ERROR

lambda 表达式的局部变量必须是事实最终变量(effectively final)(具有final特性),即lambda表达式使用的变量不可在之后被修改。

lambda表达式实际上是创建了一个对象以向方法传入。

另外,

当lambda表达式里没有使用上下文中的其他变量时,则每次执行lambda表达式都使用同一个对象。

当lambda表达式里使用了上下文中的其他变量时,则每次执行lambda表达式都会新建一个对象。

函数式接口

函数式接口:只有一个抽象方法的接口(可以有静态方法、默认方法和重写Object的公共方法)。

An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface’s abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere.
Note that instances of functional interfaces can be created with lambda expressions, method references, or constructor references.
If a type is annotated with this annotation type, compilers are required to generate an error message unless:
The type is an interface type and not an annotation type, enum, or class.
The annotated type satisfies the requirements of a functional interface.
However, the compiler will treat any interface meeting the definition of a functional interface as a functional interface regardless of whether or not a FunctionalInterface annotation is present on the interface declaration.
自:
1.8

eg. 函数式接口Comparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);

boolean equals(Object obj);

default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}

default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}

default <U> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
return thenComparing(comparing(keyExtractor, keyComparator));
}

default <U extends Comparable<? super U>> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor)
{
return thenComparing(comparing(keyExtractor));
}

default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor) {
return thenComparing(comparingInt(keyExtractor));
}

default Comparator<T> thenComparingLong(ToLongFunction<? super T> keyExtractor) {
return thenComparing(comparingLong(keyExtractor));
}

default Comparator<T> thenComparingDouble(ToDoubleFunction<? super T> keyExtractor) {
return thenComparing(comparingDouble(keyExtractor));
}

public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
return Collections.reverseOrder();
}

@SuppressWarnings("unchecked")
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}

public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
return new Comparators.NullComparator<>(true, comparator);
}

public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {
return new Comparators.NullComparator<>(false, comparator);
}

public static <T, U> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
Objects.requireNonNull(keyExtractor);
Objects.requireNonNull(keyComparator);
return (Comparator<T> & Serializable)
(c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
keyExtractor.apply(c2));
}

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}

public static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Long.compare(keyExtractor.applyAsLong(c1), keyExtractor.applyAsLong(c2));
}

public static<T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Double.compare(keyExtractor.applyAsDouble(c1), keyExtractor.applyAsDouble(c2));
}
}

如静态方法Arrays.sort的第二个参数需要一个Comparator实例,而Comparator就是一个函数式接口,所以可以提供一个lambda表达式。

下面是Arrays.sortComparator的方法原型

1
2
3
4
5
6
7
8
9
10
11
@Contract(mutates = "param1") 
public static <T> void sort(
@NotNull T[] a,
@Nullable Comparator<? super T> c) {
...
}

@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}

那么我们可以这样使用Arrays.sort

1
2
Array.sort(words,
(first, second) -> first.length() - second.length());

这其实就相当于C++11中的

1
2
3
sort(words.begin(), 
words.end(),
[](string first, string second) -> bool { return first.length() > second.length(); });

或者C++11前(没有lambda表达式,只能传实名函数)的

1
2
3
4
5
6
bool comp(string first, string second) {
return first.length() > second.length();
}
sort(words.begin(),
words.end(),
comp);

lambda表达式可以转换为函数式接口,如

1
2
3
4
5
6
var timer = new Timer (1000, 
event -> {
System.out.println("At the tone, the time is " +
Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolKit().beep();
});

方法引用

方法引用:指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。

可以理解为把方法抽取出来作为“函数”

使用方法主要有3:

  1. object::instanceMethod

    eg.

    1
    2
    3
    4
    System.out::println
    // 等价于
    x -> System.out.println(x);
    // 实例方法,提供了实例,把传入的都用来做参数
  2. Class::instanceMethod

    eg.

    1
    2
    3
    4
    5
    String::compareToIgnoreCase
    // 等价于
    (x, y) -> x.compareToIgnoreCase(y)
    // 实例方法,未提供实例,把传入参数的第一个当实例
    // 因为实例方法的第一个参数总是self
  3. Class::staticMethod

    1
    2
    3
    4
    Math::pow
    // 等价于
    (x, y) -> Math.pow(x, y)
    // 静态方法不需要实例,把传入的都用来做参数

也可以在方法中使用this, super关键字

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package edu.wanpengxu.course.lambdatest;

import java.util.Arrays;
import java.util.Comparator;

public class Test {
public static void main(String[] args) {
String[] words = {"object", "class", "lambda", "interface", "abstract"};
// Arrays.sort(words,
// (first, second) -> first.length() - second.length());
Arrays.sort(words,
Test::comp);
// Arrays.sort(words,
// String::compareTo);
// Arrays.sort(words,
// Comparator.comparingInt(String::length)); // Comparator.comparingInt(x -> x.length())
for (var word : words)
System.out.println(word);
}

public static int comp(String first, String second) {
return first.length() - second.length();
}
}

对于第一种Arrays.sort,就是直接传入lambda表达式

对于第二种Arrays.sort,是使用了自定义的方法引用

对于第三、四种Arrays.sort,是使用了Java API的方法引用

output.out

1
2
3
4
5
class
object
lambda
abstract
interface

从结果可以看出程序按照我们传入的算法进行了稳定的排序。

那么为什么可以呢?

我们首先看一下 Arrays.sort方法

1
2
3
4
5
6
7
8
9
10
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}

可以看到接收的第二个参数是Comparator类的一个实例c,如果c不为null,那么将c传入legacyMergeSort(历史归并排序)。

跟踪进入legacyMergeSort方法看看

1
2
3
4
5
6
7
private static <T> void legacyMergeSort(T[] a, Comparator<? super T> c) {
T[] aux = a.clone();
if (c==null)
mergeSort(aux, a, 0, a.length, 0);
else
mergeSort(aux, a, 0, a.length, 0, c);
}

可以看到接收的第二个参数是也Comparator类的一个实例c,如果c不为null,那么将c传入mergeSort(归并排序/合并排序)。

跟踪进入mergeSort方法看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static void mergeSort(Object[] src,
Object[] dest,
int low, int high, int off,
Comparator c) {
int length = high - low;

// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--)
swap(dest, j, j-1);
return;
}

// Recursively sort halves of dest into src
int destLow = low;
int destHigh = high;
low += off;
high += off;
int mid = (low + high) >>> 1;
mergeSort(dest, src, low, mid, -off, c);
mergeSort(dest, src, mid, high, -off, c);

// If list is already sorted, just copy from src to dest. This is an
// optimization that results in faster sorts for nearly ordered lists.
if (c.compare(src[mid-1], src[mid]) <= 0) {
System.arraycopy(src, low, dest, destLow, length);
return;
}

// Merge sorted halves (now in src) into dest
for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0)
dest[i] = src[p++];
else
dest[i] = src[q++];
}
}

在这里看到了c.compare(dest[j-1], dest[j])

跟踪进入Comparator类里面看看compare方法

1
2
3
4
5
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
...
}

可以看到,我们自定义的方法与方法签名完全一致(除了方法名外,方法参数一致,返回类型相同),所以可以传入。

同理,可以看一下String类里的compareTo方法

1
2
3
4
5
6
7
8
9
10
11
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
byte coder = coder();
if (coder == anotherString.coder()) {
return coder == LATIN1 ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}

函数签名看起来不太一样,但因为实例方法有一个隐含的this参数,String类的compareTo方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:

1
public static int compareTo(this, String o);

所以它也可以作为方法引用传入。

构造器引用

可以认为是方法名为new的方法引用,二者都是对lambda表达式的简化。

eg.

1
2
3
Student::new
// 等价于
age -> new Student(age)

此后可以使用apply方法向其传入参数。

内部类

内部类(inner class):定义在另一个类中的类。

使用内部类的原因:

  1. 内部类可以对同一个包中的其他类隐藏
  2. 内部类方法可以访问外围类的作用域中所有的数据,包括原本私有的数据
  3. 便于回调(以后补)

语法格式

1
2
3
4
5
6
class OuterClass {   // 外部类
// ...
class InnerClass { // 嵌套类,或称为内部类
// ...
}
}

内部类与外围类的字段、方法处于相同地位(都是外围类的成员)

内部类可以直接访问外围类成员

非静态内部类

外围类不能直接使用非静态内部类的成员,但可以创建一个内部类的实例,通过这个实例访问内部类的成员,有两种实现方法。

  1. 在外围类内的方法中创建,与创建一般对象相同

    1
    {内部类类名} {内部类变量} = new {内部类构造方法名}({参数列表});
  2. 在外围类外创建,需要使用如下格式

    1
    {外部类实例}.{内部类类名} {内部类变量} = {外部类实例}.new {内部类构造方法名}({参数列表});

    即在两侧左边同时加了{外部类实例}.以做限定

若使用 privateprotected 来修饰内部类,那么方法2将失效。

eg. PPT魔改例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package edu.wanpengxu.course.innerclasstest;

public class OuterClass {
private int i1;

class InnerClass {
private final String label;

InnerClass(String whereTo) {
label = whereTo;
}

String readLabel() {
return label + (i1++);
}
}

public void ship(String dest) {
InnerClass i = new InnerClass(dest);
System.out.println(i.readLabel());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package edu.wanpengxu.course.innerclasstest;

public class Test {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
for (int i = 0; i <= 4; i++)
outerClass.ship("1Test");
OuterClass.InnerClass innerClass = outerClass.new InnerClass("2Test");
for (int i = 5; i <= 9; i++)
System.out.println(innerClass.readLabel());
}
}
1
2
3
4
5
6
7
8
9
10
1Test0
1Test1
1Test2
1Test3
1Test4
2Test5
2Test6
2Test7
2Test8
2Test9

静态内部类

可以在外部类外直接创建静态内部类

静态内部类无法直接访问外部类的非static成员,但可以通过创建外部类实例访问,如eg2。

eg. PPT魔改例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package edu.wanpengxu.course.innerclasstest2;

class OuterClass {
private final int a = 1;
private static final int b = 2;

static class InnerClass {
public final int c = 3;

static void show() {
// System.out.println("OuterClass's a: " + a); // ERROR
System.out.println("OuterClass's b: " + b);
}
}
}

public class Test {
public static void main(String[] args) {
OuterClass.InnerClass.show();
OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
System.out.println(innerClass.c);
}
}

eg2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package edu.wanpengxu.course.innerclasstest3;

class OuterClass {
private final String s1 = "I am in OuterClass";
private static final String s2 = "static in OuterClass";

public static class InnerClass {
private final String s1 = "I am in InnerClass";
private final String s3 = new OuterClass().s1;

public void display() {
System.out.println(s1);
System.out.println(this.s1);
System.out.println(s3);
System.out.println(s2);
}
}
}

public class Test {
public static void main(String[] args) {
OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
// System.out.println(innerClass instanceof OuterClass); // ERROR 内部类不是外围类的子类
innerClass.display();
}
}
1
2
3
4
I am in InnerClass
I am in InnerClass
I am in OuterClass
static

匿名内部类

匿名内部类(anonymous inner class)

这种方法最好使用lambda表达式替代,如lambda表达式那一节的截图一般。

假设有一个超类

1
2
3
4
5
class Class1 {
public void display() {
System.out.println("I am Class1");
}
}

一般的类创建实例的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
class Class2 extends Class1 {
@Override
public void display() {
System.out.println("I am Class2");
}
}

public class Test {
public static void main(String[] args) {
Class1 commonClassObject = new Class2();
commonClassObject.display();
}
}

匿名内部类创建实例的方法

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
Class1 anonymousInnerClassObject = new Class1() {
@Override
public void display() {
System.out.println("I am Class3");
}
};
anonymousInnerClassObject.display();
}
}

可以看到减少了不少代码量

完整测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package edu.wanpengxu.course.anonymousinnerclasstest;

class Class1 {
public void display() {
System.out.println("I am Class1");
}
}

class Class2 extends Class1 {
@Override
public void display() {
System.out.println("I am Class2");
}
}

public class Test {
public static void main(String[] args) {
Class1 commonClassObject = new Class2();
commonClassObject.display();

Class1 anonymousInnerClassObject = new Class1() {
@Override
public void display() {
System.out.println("I am Class3");
}
};
anonymousInnerClassObject.display();
}
}
1
2
I am Class2
I am Class3

事实上除了创建类的实例,还可以创建接口的实例。

12.9 异常

异常

12.27 并发