接口、lambda表达式与内部类

1. 接口

1.1 接口的概念

接口(Interface)是一种引用类型,它是方法和常量的集合。从本质上讲,接口是一种全抽象的类,也就是说,它不能实例化。一个类可以实现(implement)一个或者多个接口,这样这个类就继承了接口的抽象方法,这些方法就需要在该类中具体实现。

接口定义的语法如下:

public interface MyInterface {
    // Any number of final, static fields
    // Any number of abstract method declarations\
}

接口主要有两个目的:

  1. 确保符合某个特定的协议:一个类如果实现一个接口,那么就必须实现接口中声明的所有方法。接口强制实现类遵循某种规定的行为模式。
  2. 实现多重继承:Java 不支持类的多继承,但接口可以继承其他接口,并且一个类可以实现多个接口。这就可以达到类似多继承的效果。

需要注意的是,Java 8 引入了接口默认方法和静态方法的概念,这使得接口具有某种程度的“行为”。同时,Java 9 又引入了私有方法和私有静态方法。然而,接口仍不能包含任何状态(即,接口不能包含非 final 或者非 static 的字段)。

Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下面这个条件:对象所属的类必须实现Comparatble接口

Comparatble接口,compareTo方法是抽象的,没有具体实现,任何实现Comparable接口的类都需要包含一个compareTo方法

  • 接口中所有方法都自动是public方法
  • 接口没有实例,更没有实例字段
public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public int compareTo(Person other) {
        return this.age - other.age;
    }
}

//在这个例子中,compareTo 方法返回了两个 Person 对象年龄的差值。这样,就定义了一种按照年龄比较 Person 对象的方式。
//当 compareTo 方法返回一个负数时,说明 this 对象小于 other 对象;当 compareTo 方法返回 0 时,说明 this 对象和 other 对象相等;当 compareTo 方法返回一个正数时,说明 this 对象大于 other 对象。



为什么不直接在Person类中直接提供一个compareTo方法?

  1. 强制实现:通过实现 Comparable 接口,Java 强制类实现者提供 compareTo 方法的实现,这在编译阶段就能够检查到,从而避免在运行时出现方法未找到的错误。
  2. 通用性:许多 Java 标准库中的类和方法,如排序方法 Arrays.sort() 和集合类 TreeSetTreeMap 等,都需要操作对象提供 compareTo 方法以便能够比较对象。这些类和方法期望接收的参数是 Comparable 类型,只有实现了 Comparable 接口的类才能被这些方法接受。
  3. 可读性和清晰性:实现 Comparable 接口清晰地表明类的实例有自然顺序,可以进行比较和排序。

1.2 接口的属性

  • 接口不是类,不能使用new操作符实例化一个接口,但仍然能声明接口变量
  • 接口变量必须引用实现了这个接口的一个类对象
  • 使用instanceof检查一个对象是否实现了某个特定的接口
  • 与建立类的继承层次结构一样,也可以扩展接口,接口可以继承
  • 接口可以有属性,但是这些属性都是隐式地被设定为 public, staticfinal。这意味着接口的属性是全局常量,不可以被修改。

java有一个内置接口,名为Cloneable,如果类实现了这个接口,Object类中的clone方法就可以创建你的类对象的一个完全副本,如果你希望自己的类既能够克隆又能够比较,只要实现这两个接口就可以了

class Employee implements Cloneable,Compatable

1.3 接口与抽象类

接口和抽象类在 Java 中都有其特定的用途和角色,尽管他们在某些方面有相似性,但他们也有一些关键的不同。下面是一些区别和为什么 Java 需要接口的理由:

  1. 多重继承:在 Java 中,一个类只能继承自一个父类,这就是所谓的单继承。但是,一个类可以实现多个接口。这种设计方式提供了一种间接的多重继承机制,让你可以定义出一些具有共同行为的不同类,且每个类还可以拥有其自身的特性和行为。
  2. 灵活性:接口允许类保持其自己的层次结构,同时还能实现共享的公共行为。比如,Comparable 可以被任何可以比较的类实现,无论这些类在继承树中处于何处。如果 Comparable 是一个抽象类,那么所有想要实现比较功能的类都必须从这个抽象类继承,这就限制了这些类的设计和层次结构。
  3. 规范行为:接口定义了一种约定或者协议,它描述了类应该做什么,而不是怎么去做。这是面向对象设计的一个重要原则,称为"对接口编程,而不是对实现编程"。这种方式使得代码更具有灵活性,因为你可以更改或替换实现,而不影响到使用这些实现的代码。

在总结,抽象类是一种 "是什么" 的关系,而接口定义的是一种 "能做什么" 的能力。Java 中的类只能继承一个父类,但是可以实现多个接口,这就使得 Java 能够充分利用这两种关系来进行更灵活和强大的设计。

1.4 静态和私有方法

接口中的方法通常是抽象的,即它们只有声明而没有实现。然而,从 Java 8 开始,接口可以包含静态方法和默认方法,并在 Java 9 中添加了私有方法。让我们详细介绍这两种方法:

  1. 公共静态方法:静态方法与接口类型的实例无关,通常用于执行与接口类型相关的、但不依赖于接口实例的工作。它们在接口中是公共的,可直接通过接口调用,但无法在实现类中被重写。下面是一个例子:

    public interface MyInterface {
        static void myMethod() {
            System.out.println("This is a public static method in the interface.");
        }
    }
    

    你可以通过 MyInterface.myMethod(); 来调用这个静态方法。

  2. 私有方法:从 Java 9 开始,接口可以有私有方法。私有方法仅仅在接口内部使用,它们主要用于提供接口中的默认方法或其他静态方法的公共部分,从而提高代码复用性。私有方法不能在接口外部被访问,包括实现类。下面是一个例子:

    public interface MyInterface {
        default void defaultMethod() {
            privateMethod();
        }
    
        private void privateMethod() {
            System.out.println("This is a private method in the interface.");
        }
    }
    

    在这个例子中,privateMethod() 只能在 MyInterface 接口中被调用。它不能被实现 MyInterface 接口的类调用或重写。

所以,接口中的公共静态方法和私有方法提供了一种在接口中封装代码的方式,同时使代码保持整洁和可读。

1.5 默认方法

从 Java 8 开始,接口可以包含默认方法。默认方法是一种具有默认实现的方法,它允许我们在接口中添加新的方法,而不会打破已经实现该接口的现有类的代码。

默认方法使用 default 关键字进行声明,并提供一个方法体。以下是一个例子:

public interface MyInterface {
    default void defaultMethod() {
        System.out.println("This is a default method.");
    }
}

在上述例子中,defaultMethod 是一个默认方法。任何实现 MyInterface 接口的类都可以选择是否重写这个方法。如果一个类实现了 MyInterface 接口但没有提供 defaultMethod 方法的实现,那么这个类就会自动继承 defaultMethod 的默认实现。

默认方法的引入使得我们可以向接口添加新的方法,而不会破坏已经存在的实现。这是一个非常有用的特性,因为在以前,如果我们向接口添加新的方法,那么所有实现了这个接口的类都必须进行修改以提供这个新方法的实现。

默认方法还有另一个重要的用途,那就是它允许我们在接口中编写可选的方法。换句话说,实现接口的类可以选择是否实现这个方法。如果选择不实现这个方法,那么就会使用默认方法的实现。

1.6 解决默认方法冲突

当一个类实现的多个接口中存在相同的默认方法时,就会出现冲突。Java 提供了几种策略来解决这种默认方法冲突:

  1. 类始终优先:如果一个类或其父类中提供了一个具体的方法,那么这个方法将会被使用,而不是接口中的默认方法。
  2. 接口冲突:如果一个类实现了多个接口,而这些接口中有相同的默认方法,那么类必须覆盖这个冲突的默认方法。

例如,假设有两个接口 InterfaceAInterfaceB,它们都有一个相同的默认方法 defaultMethod

public interface InterfaceA {
    default void defaultMethod() {
        System.out.println("Default method from InterfaceA");
    }
}

public interface InterfaceB {
    default void defaultMethod() {
        System.out.println("Default method from InterfaceB");
    }
}

现在,如果有一个类 MyClass 同时实现了 InterfaceAInterfaceB,那么它必须覆盖 defaultMethod 方法来解决冲突:

public class MyClass implements InterfaceA, InterfaceB {
    @Override
    public void defaultMethod() {
        // 需要明确选择使用哪一个接口的默认方法
        InterfaceA.super.defaultMethod();
    }
}

在这个例子中,MyClass 明确选择使用 InterfaceAdefaultMethod 方法。这样就解决了默认方法的冲突。

总的来说,当一个类实现的多个接口有相同的默认方法时,类必须明确选择使用哪一个接口的默认方法,或者提供一个新的实现。这是 Java 对于默认方法冲突的解决策略。

1.7 接口与回调

接口在 Java 中被广泛用于实现回调机制。回调是一种常见的编程模式,允许我们指定某个特定事件发生时应该调用的代码。在很多情况下,我们可以使用 Java 接口作为回调函数的类型,然后使用这个接口的实现类来提供回调函数的具体实现。

下面是一个简单的例子,我们定义了一个接口 Callback,并使用这个接口来实现回调:

public interface Callback {
    void call();
}

public class MyClass {
    private Callback callback;

    public MyClass(Callback callback) {
        this.callback = callback;
    }

    public void doSomething() {
        System.out.println("Doing something...");
        callback.call();
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass myClass = new MyClass(new Callback() {
            @Override
            public void call() {
                System.out.println("Callback called!");
            }
        });
        myClass.doSomething();
    }
}

在这个例子中,MyClass 类的 doSomething 方法在完成一些工作后会调用回调函数。这个回调函数是通过 Callback 接口提供的,所以我们可以在创建 MyClass 对象时提供一个自定义的回调函数。

使用接口实现回调有很多优点。首先,它使我们的代码更加灵活和可重用,因为我们可以很容易地更换不同的回调函数。其次,它使我们的代码更加清晰和易于理解,因为回调函数的签名(参数类型和返回类型)都在接口中明确地定义了。

然而,使用接口实现回调也有一些局限性。例如,我们无法使用接口实现多个回调函数,除非我们为每个回调函数定义一个新的接口。此外,使用接口实现回调可能会使我们的代码变得更复杂,尤其是当我们需要定义大量的回调接口时。这些问题在一些情况下可以通过使用 Java 8 的 Lambda 表达式或方法引用来解决。

1.8 Comparator接口

Comparator 是 Java 中的一个功能接口,主要用于定义对象排序的规则。Comparator 接口定义了一个名为 compare 的方法,该方法接收两个参数,返回一个 int 类型的值。

如果我们需要对一些不能自然排序,或者我们需要使用不同于其自然排序的规则来排序对象,那么就可以使用 Comparator

以下是 Comparator 的基本使用示例:

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

public class Main {
    public static void main(String[] args) {
        String[] fruits = {"Pineapple", "Apple", "Orange", "Banana"};

        Arrays.sort(fruits, new Comparator<String>(){
            @Override
            public int compare(String fruit1, String fruit2) {
                return fruit1.length() - fruit2.length();
            }
        });

        System.out.println(Arrays.toString(fruits));
        // 输出:[Apple, Orange, Banana, Pineapple]
    }
}

在上述代码中,我们创建了一个字符串数组,并使用 Comparator 排序了这个数组。排序的规则是按照字符串的长度进行排序。Comparatorcompare 方法返回的结果决定了排序的顺序:如果返回的结果小于 0,那么第一个参数在排序后会在第二个参数之前;如果返回的结果等于 0,那么两个参数的顺序不变;如果返回的结果大于 0,那么第一个参数在排序后会在第二个参数之后。

从 Java 8 开始,Comparator 还提供了许多有用的默认方法(如 reversedthenComparing 等)和静态方法(如 naturalOrdernullsFirstcomparing 等),这使得我们可以更方便地创建和使用 Comparator。例如:

Comparator<String> comparator = Comparator.comparingInt(String::length);

在上述代码中,我们使用了 ComparatorcomparingInt 静态方法,根据字符串的长度创建了一个 Comparator。这样的代码比创建一个匿名类更简洁,更易读。

1.9 对象克隆

Cloneable接口是一个标记接口,它本身并没有定义任何方法。在Java中,当类实现了 Cloneable 接口,那么它的对象就能被克隆(即创建一个与原对象内容完全相同的新对象)。对象克隆的过程是通过调用 Object 类的 clone 方法来实现的。

以下是一个实现 Cloneable 接口的示例:

public class Person implements Cloneable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 重写 clone 方法
    @Override
    public Object clone() {
        try {
            // 调用 Object 类的 clone 方法
            return super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // 不应该发生
        }
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("Alice", 20);
        Person person2 = (Person) person1.clone();
        System.out.println(person1); // 输出:Person [name=Alice, age=20]
        System.out.println(person2); // 输出:Person [name=Alice, age=20]
    }
}

在这个例子中,Person 类实现了 Cloneable 接口,并重写了 Object 类的 clone 方法。这样,我们就可以创建 Person 对象的副本了。注意,在调用 clone 方法时,可能会抛出 CloneNotSupportedException 异常,这个异常通常表示该类不支持克隆操作。但在这个例子中,我们知道 Person 类已经支持克隆操作(因为它实现了 Cloneable 接口),所以如果 clone 方法抛出了 CloneNotSupportedException 异常,那么肯定是出现了严重的错误。

需要注意的是,clone 方法创建的是对象的浅拷贝,也就是说,如果对象的字段是引用类型,那么 clone 方法只会复制引用,而不会复制引用所指向的对象。如果你需要创建对象的深拷贝,那么可能需要重写 clone 方法,以正确地复制对象的字段。

2. lambda表达式

2.1 为什么引入lambda表达式

  1. 简化代码:Lambda表达式使得编写函数式编程变得更加简洁,可以避免冗长的匿名内部类的写法。
  2. 提高代码可读性:Lambda表达式的语法更简洁,更接近于自然语言,提高了代码的可读性。
  3. 支持函数式编程:Java一直以来都是一种面向对象的编程语言。引入Lambda表达式之后,Java可以更好地支持函数式编程,让开发者能够使用更加灵活的编程方式。
  4. 并行处理:Lambda表达式配合Java 8新引入的流(Stream)API,可以更方便地进行并行和并发处理,使得多核处理器的优势得到更好的发挥。
  5. 提升集合操作性能:通过结合流API,Lambda表达式使得对集合的操作更为简洁高效,可以在集合上进行更复杂的操作,例如过滤、映射、聚合等。
  6. 与新的API交互:Java 8还引入了许多新的API,例如Optional、Stream等,这些API的设计都高度依赖Lambda表达式。

2.2 lambda表达式语法

Java的lambda表达式主要用于实现函数式接口,即只有一个抽象方法的接口。它提供了一种简洁的方式来表示这些接口的实例。

一个Lambda表达式主要包括以下几个部分:

  • 一个括号内的参数列表:这个参数列表对应函数式接口中的抽象方法的参数列表。如果没有参数,括号内为空;如果有一个参数,括号可以省略。
  • 一个箭头符号:->,这个箭头符号将参数列表与Lambda体(Lambda表达式的主体)分隔开。
  • 一个Lambda体:这个Lambda体表示了当函数式接口的抽象方法被调用时应该执行的操作。如果Lambda体只有一行代码,可以省略大括号;如果Lambda体包括了多行代码,那么必须使用大括号包围。

下面是一些Lambda表达式的示例:

() -> System.out.println("Hello World");  // 无参数,Lambda体是一个打印语句

(String s) -> System.out.println(s);  // 一个参数,可以省略参数类型和括号:s -> System.out.println(s)

(int a, int b) -> a + b;  // 两个参数,Lambda体是一个表达式,结果是两个参数的和

(s, i) -> {  // 两个参数,Lambda体是一个代码块
    if (s.length() > i) {
        return s.charAt(i);
    } else {
        return '\0';
    }
}

这些Lambda表达式都可以用作函数式接口的实例。例如,(String s) -> System.out.println(s)可以用作一个java.util.function.Consumer<String>的实例,而(int a, int b) -> a + b可以用作一个java.util.function.BinaryOperator<Integer>的实例。

2.3 函数式接口

函数式接口(Functional Interface)是 Java 8 的一个新特性。它是一种只有一个抽象方法的接口。因为在 Java 中,我们可以用 Lambda 表达式来代表只有一个抽象方法的接口的实例。

尽管我们可以自己定义函数式接口,但是 Java 8 也在 java.util.function 包中已经提供了很多常用的函数式接口。比如:

  • Predicate<T>:接收一个 T 类型的参数,返回一个 boolean 类型的结果,通常用于做条件判断。
Predicate<String> predicate = s -> s.length() > 0;
boolean testResult = predicate.test("Hello");  // 结果为 true
  • Function<T, R>:接收一个 T 类型的参数,返回一个 R 类型的结果,通常用于做转换操作。
Function<String, Integer> function = s -> Integer.parseInt(s);
Integer applyResult = function.apply("123");  // 结果为 123
  • Consumer<T>:接收一个 T 类型的参数,没有返回结果,通常用于做一些消费操作,比如输出。
Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("Hello");  // 输出 "Hello"
  • Supplier<T>:不接收参数,返回一个 T 类型的结果,通常用于做一些生产操作。
Supplier<String> supplier = () -> "Hello";
String getResult = supplier.get();  // 结果为 "Hello"

在 Java 8 中,有一个 @FunctionalInterface 注解,它是可选的,但如果一个接口被这个注解标记,那么编译器就会强制检查这个接口是否真的只有一个抽象方法,如果不是的话,编译器会报错。

2.4 方法引用

Java 方法引用是一种简化 Lambda 表达式的语法,它允许你直接引用已经存在的方法,以代替Lambda表达式来传递代码块。方法引用使得代码更加简洁,易于理解,同时提供了更高的可读性。它通常用于函数式接口,即只有一个抽象方法的接口。

方法引用的语法格式为:

ClassName::methodName

或者

objectReference::methodName

其中:

  • ClassName 是包含目标方法的类名。
  • methodName 是需要引用的方法名称。

方法引用可以分为以下四种情况:

  1. 静态方法引用:ClassName::staticMethodName
  2. 实例方法引用:objectReference::instanceMethodName
  3. 特定类型的任意对象方法引用:ClassName::instanceMethodName
  4. 构造方法引用:ClassName::new

让我们逐个解释这些情况:

2.4.1 静态方法引用

静态方法引用用于引用一个类的静态方法。语法为 ClassName::staticMethodName

// Lambda 表达式
Function<Integer, Integer> square = (x) -> Math.pow(x, 2);

// 静态方法引用
Function<Integer, Integer> square = Math::pow;

2.4.2 实例方法引用

实例方法引用用于引用某个对象的实例方法。语法为 objectReference::instanceMethodName

// Lambda 表达式
Consumer<String> printUpperCase = (s) -> System.out.println(s.toUpperCase());

// 实例方法引用
Consumer<String> printUpperCase = System.out::println;

2.4.3 特定类型的任意对象方法引用

特定类型的任意对象方法引用用于引用特定类型的任意对象的实例方法。语法为 ClassName::instanceMethodName

// Lambda 表达式
Predicate<String> startsWithHello = (s) -> s.startsWith("Hello");

// 特定类型的任意对象方法引用
Predicate<String> startsWithHello = String::startsWith;

2.4.4 构造方法引用

构造方法引用用于引用一个类的构造方法,创建该类的新实例。语法为 ClassName::new

// Lambda 表达式
Supplier<ArrayList<String>> listSupplier = () -> new ArrayList<>();

// 构造方法引用
Supplier<ArrayList<String>> listSupplier = ArrayList::new;

注意:方法引用只适用于函数式接口,即只有一个抽象方法的接口。在使用方法引用时,方法签名必须与函数式接口中的抽象方法签名相匹配。

方法引用是一种更加简洁和易读的编程方式,使得代码更加优雅和可维护。它是 Java 8 中强大的功能之一,能够在函数式编程中大大简化代码。

2.5 构造器引用

Java 构造器引用是一种方法引用的形式,用于引用类的构造器(构造方法)。它可以让你在函数式接口的上下文中直接使用类的构造器来创建新的对象实例。构造器引用的语法与其他方法引用类似,使用 ClassName::new 来引用构造器。

构造器引用适用于以下场景:

  1. 当目标函数式接口的抽象方法参数列表与构造器的参数列表相匹配时。
  2. 当需要创建类的新实例作为函数式接口的返回值时。

以下是构造器引用的几种常见用法:

2.5.1 无参构造器引用:

// Lambda 表达式
Supplier<String> supplier1 = () -> new String();

// 构造器引用
Supplier<String> supplier2 = String::new;

在这个例子中,Supplier<String> 是一个函数式接口,它的抽象方法 T get() 返回一个 String 对象。通过构造器引用 String::new,我们可以直接使用 String 类的无参构造器来创建新的字符串实例。

2.5.2 有参构造器引用:

// Lambda 表达式
Function<Integer, ArrayList<String>> function1 = (size) -> new ArrayList<>(size);

// 构造器引用
Function<Integer, ArrayList<String>> function2 = ArrayList::new;

在这个例子中,Function<Integer, ArrayList<String>> 是一个函数式接口,它的抽象方法 R apply(T t) 接收一个整数参数并返回一个 ArrayList<String> 对象。通过构造器引用 ArrayList::new,我们可以直接使用 ArrayList 类的有参构造器来创建新的 ArrayList 实例。

构造器引用是一种简洁而强大的技术,它可以在函数式编程中提供更加优雅和简单的对象实例化方式。它与其他方法引用一起,使得 Java 8 及以上版本中的函数式编程更加灵活和易于理解。

2.6 变量作用域

Lambda 表达式的作用域与包含它的外部作用域相同,且Lambda 表达式可以访问其外部作用域的变量。然而,有些限制规则要注意:

  1. 实例字段和静态变量:Lambda 表达式可以直接访问外部的实例字段和静态变量,而且在Lambda 表达式内部,我们甚至可以修改这些字段和静态变量。
  2. 局部变量:Lambda 表达式可以访问其外部作用域的 final 局部变量或 "effectively final" 局部变量。"effectively final" 是指这个局部变量在初始化之后就没有被进一步改变过。Java要求这种访问模式的原因是,由于实例字段和静态变量存储在堆内存中,而Lambda 表达式会在另一个线程中执行,因此为了避免数据不一致的问题,如果允许在Lambda 表达式中改变局部变量,那么可能会出现意想不到的结果。如果你在 Lambda 表达式内部尝试改变局部变量的值,编译器将给出错误信息。
int outerVariable = 10; //这是一个 "effectively final" 的局部变量

Consumer<Integer> myLambda = (innerVariable) -> {
    System.out.println(innerVariable + outerVariable); //读取外部局部变量
    // outerVariable = 20; //取消此行注释将导致编译错误,因为 outerVariable 是 "effectively final" 的
};
myLambda.accept(50); // 输出 60
  1. Lambda 内部的变量:在 Lambda 表达式内部声明的变量只在该Lambda 表达式内部有效,就像在传统方法内部声明的变量一样。
  2. this关键字:在 Lambda 表达式内部,this关键字的含义与在Lambda 表达式的封闭作用域内的含义是相同的。也就是说,this关键字并不指向Lambda 表达式自身,因为Lambda 表达式不是一个实体,而是指向封闭这个Lambda 表达式的类的当前实例。例如,如果你在一个类的成员方法中创建一个Lambda 表达式,那么在这个Lambda 表达式中使用 this关键字,它将引用类的当前实例。

2.7 处理lambda表达式

Java 的 Lambda 表达式是一种在 Java 8 中引入的新特性,它们为 Java 提供了一种清晰简洁的方式来表示函数式接口(只有一个抽象方法的接口)。Lambda 表达式可以用来创建匿名方法,即没有声明的方法。这种表达方式使得代码变得更加简洁,更易读。

以下是创建和使用 Lambda 表达式的一般步骤:

  1. 定义函数式接口:首先,你需要一个函数式接口,作为 Lambda 表达式的目标类型。例如,Java 标准库中的 java.util.function.Consumer 是一个常用的函数式接口:
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
  1. 创建 Lambda 表达式:Lambda 表达式的语法是 (参数) -> { 函数体 }。如果函数体只有一条语句,那么可以省略大括号。如果参数只有一个,也可以省略圆括号。
Consumer<String> greet = (name) -> System.out.println("Hello, " + name);
  1. 使用 Lambda 表达式:你可以像使用普通的 Java 对象一样使用 Lambda 表达式:
greet.accept("World"); // 输出 "Hello, World"

在 Java 中,Lambda 表达式的类型是一个函数式接口。Java 运行时将 Lambda 表达式转换为该接口的一个实例。这种转换过程完全由 Java 编译器和运行时系统自动完成,对开发者来说是透明的。这意味着你可以将 Lambda 表达式赋值给一个函数式接口类型的变量,也可以将 Lambda 表达式作为参数传递给一个接受函数式接口类型的方法。

2.8 再谈Comparator

Comparator 是 Java 中的一个接口,它包含在 java.util 包中。这个接口定义了一种排序对象的方法。它包含两个函数:

  • compare(T o1, T o2):这个函数需要你提供两个你想比较的对象,然后返回一个整数。如果 o1 小于 o2,返回负数;如果 o1 等于 o2,返回0;如果 o1 大于 o2,返回正数。
  • equals(Object obj):这个函数判断是否和传入的对象相等。不过在实际应用中,这个函数用得较少,一般默认使用 Object 类中的 equals 方法。

在 Java 8 中,Comparator 接口还增加了一些新的默认方法和静态方法,如 reversed()thenComparing()naturalOrder() 等,这些方法让我们可以更方便地进行复杂的比较和链式比较。

这是一个使用 Comparator 的例子:

List<String> words = Arrays.asList("Apple", "Banana", "Cherry");

// 使用匿名内部类创建 Comparator
Collections.sort(words, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

// 使用 Lambda 表达式创建 Comparator
Collections.sort(words, (s1, s2) -> s1.length() - s2.length());

// 使用 Comparator.comparing 创建 Comparator
words.sort(Comparator.comparing(String::length));

这个例子中,我们创建了三个 Comparator 来根据字符串的长度排序一个字符串列表,这三个 Comparator 是等效的。使用 Lambda 表达式或 Comparator.comparing 可以让代码更简洁。

3. 内部类

内部类时定义在另一个类中的类,为什么使用内部类?

  • 内部类可以对同一个包中的其他类隐藏
  • 内部类方法可以访问定义这些方法的作用域中的数据,包括原本私有的数据

3.1 使用内部类访问对象状态

在 Java 中,内部类(也称为嵌套类)是定义在另一个类的内部的类。非静态内部类(也称为成员内部类)具有一个外部类对象的引用,因此它可以访问外部类的所有成员(包括私有成员)。这让我们可以方便地在内部类中修改或访问外部类的状态。

下面是一个简单的例子:

public class OuterClass {
    private int count = 0;

    public class InnerClass {
        public void incrementCount() {
            count++; // 内部类访问和修改外部类的状态
        }

        public int getCount() {
            return count; // 内部类访问外部类的状态
        }
    }

    public InnerClass createInnerClass() {
        return new InnerClass();
    }
}

OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.createInnerClass();
innerClass.incrementCount();
System.out.println(innerClass.getCount()); // 输出 1

在这个例子中,我们定义了一个 OuterClass 和一个 InnerClassInnerClass 可以访问和修改 OuterClasscount 成员。

然而,尽管内部类可以访问外部类的所有成员,但是反过来则不行。也就是说,外部类不能直接访问内部类的成员,除非你已经有了一个内部类的实例。

此外,静态内部类(也称为静态嵌套类)并没有外部类对象的引用,因此它不能直接访问外部类的非静态成员。但是它可以访问外部类的静态成员。静态内部类的一个常见用途是作为外部类的辅助类,处理外部类的静态操作。

3.2 内部类的特殊语法规则

Java中的内部类(或称为嵌套类)有一些特殊的语法规则和考虑因素:

  1. 实例化:非静态内部类的实例必须与外部类的实例关联。要创建一个非静态内部类的实例,你首先需要创建一个外部类的实例。例如:
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

对于静态内部类,你不需要一个外部类的实例,可以直接实例化:

OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
  1. 访问权限:内部类可以访问外部类的所有成员,包括私有成员。这是因为内部类是外部类的成员,所以它享有同样的访问权限。但是,外部类不能访问内部类的成员,除非你已经有了一个内部类的实例。
  2. this 关键字:在内部类中,this 关键字引用的是内部类的实例。如果你需要引用外部类的实例,可以使用 OuterClass.this
  3. 静态成员:非静态内部类不能有静态成员,因为非静态内部类的对象总是依赖于外部类的实例。静态内部类(也称为静态嵌套类)则没有这个限制,它可以有静态成员。
  4. 序列化:如果一个内部类是可序列化的,那么它的外部类也应该是可序列化的。这是因为内部类总是持有外部类的引用。
  5. 局部内部类和匿名内部类:局部内部类是在一个块(通常是一个方法)中定义的类。匿名内部类是一种没有名字的局部内部类,通常用于需要只使用一次的场景。这两种类型的内部类都只能访问 final 或 effectively final 的局部变量。

3.3 内部类是否有用、必要和安全

Java 的内部类或嵌套类是一个强大的特性,它可以提供一些有用的功能,但其使用并非总是必要的,并且在某些情况下,可能需要注意一些安全性的问题。下面是一些关于内部类的思考:

有用性

  • 封装:内部类可以访问外部类的所有成员(包括私有成员),这使得我们可以在内部类中封装一些只有外部类需要的逻辑。
  • 增加代码的清晰性和可读性:内部类把相关的功能放在一起,这可以使代码更容易理解和维护。如果一个类只被一个其他类使用,那么将它作为一个内部类可以使代码的结构更清晰。
  • 支持更灵活的继承:每个内部类都能独立地继承自一个(接口的)实现,所以内部类使得多重继承的解决方案变得更完整。
  • 适应事件驱动编程模型:内部类常常在事件驱动编程和设计模式实现中使用,例如在 GUI 编程和访问者设计模式中。

必要性

虽然内部类提供了一些有用的功能,但在很多情况下,你可能不需要使用它。你可以使用其他方式来实现相同的功能,例如通过创建一个新的类或者使用回调。使用内部类或不使用内部类取决于你的具体需求和设计考虑。

安全性

内部类可以访问外部类的所有成员,包括私有成员。这在某种程度上违反了封装原则,可能会导致数据被误用或者错误地修改。因此,在使用内部类时,你应该注意防止数据被错误地访问或修改。

此外,内部类(特别是匿名内部类)可能会引入对外部对象的隐式引用。如果你的内部类生命周期比外部类长(例如,你把一个内部类实例赋给了一个静态变量),那么这可能会导致内存泄漏,因为外部类实例无法被垃圾回收。在使用内部类时,你需要注意这种潜在的内存泄漏问题。

3.4 局部内部类

在Java中,局部内部类(也称为局部类)是在一个代码块中定义的类。这个代码块可以是一个方法,也可以是一个作用域块。局部内部类只在其定义的代码块中可见,无法在其外部类的其他地方访问。

以下是一个局部内部类的示例:

public class OuterClass {
    public void doSomething() {
        class LocalInnerClass {
            public void printMessage() {
                System.out.println("Hello from the local inner class!");
            }
        }

        LocalInnerClass localInnerClass = new LocalInnerClass();
        localInnerClass.printMessage(); // 输出 "Hello from the local inner class!"
    }
}

在此例中,LocalInnerClass 是在 doSomething 方法中定义的局部内部类。

局部内部类有一些特殊的规则和限制:

  1. 局部内部类不能有访问修饰符(如 publicprivate 等),因为它不是其外部类的成员,而是一个局部变量。
  2. 局部内部类不能定义静态成员,因为它本身不是静态的。
  3. 和其他内部类一样,局部内部类可以访问其外部类的所有成员。然而,局部内部类只能访问其定义的代码块中的 final 或 "effectively final" 局部变量。"effectively final" 是指这个局部变量在初始化之后就没有被进一步改变过。

局部内部类主要用于实现只在一个地方使用的类。这种类通常不需要重新使用,也不需要其他地方知道它的存在。因此,将它定义为局部内部类可以使代码更加清晰和简洁。

3.5 由外部方法访问变量

局部内部类可以访问其定义所在的方法中的局部变量,但是有一些限制。如果一个局部内部类访问一个在其外部方法中定义的局部变量,那么这个局部变量必须是 final 或者 "effectively final"。

"effectively final" 是指这个局部变量在初始化之后就没有被进一步改变过。换句话说,即使你没有明确地将这个局部变量声明为 final,只要你没有改变它的值,它就是 "effectively final"。

以下是一个例子:

public class OuterClass {
    public void doSomething() {
        final int finalVariable = 1; // 显式声明为 final 的局部变量
        int effectivelyFinalVariable = 2; // "effectively final" 的局部变量

        class LocalInnerClass {
            public void printMessage() {
                System.out.println(finalVariable); // 访问 final 局部变量
                System.out.println(effectivelyFinalVariable); // 访问 "effectively final" 局部变量
            }
        }

        LocalInnerClass localInnerClass = new LocalInnerClass();
        localInnerClass.printMessage(); // 输出 "1" 和 "2"
    }
}

在这个例子中,LocalInnerClass 是一个局部内部类,它可以访问 doSomething 方法中的 finalVariableeffectivelyFinalVariable 局部变量。

这个规则的主要原因是,当你在一个方法中创建一个内部类的对象时,这个对象可能会在方法返回后继续存在。如果内部类对象可以访问和修改方法中的局部变量,那么在方法返回后,这些局部变量的状态将变得不确定,这可能会导致错误。所以,Java 要求局部内部类只能访问 final 或 "effectively final" 的局部变量,这样可以保证这些局部变量的值在方法返回后不会改变。

3.6 匿名内部类

匿名内部类是一种没有类名的内部类。它是在创建对象的地方直接定义的,通常用于需要创建一次性的对象,如实现回调、运行在GUI中的事件处理器等。

匿名内部类通常继承一个类或实现一个接口,然后提供对其方法的实现。下面是一个简单的匿名内部类的例子:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello from the anonymous inner class!");
    }
}).start();

在这个例子中,我们创建了一个新的 Thread 对象,并传入了一个匿名内部类作为参数。这个匿名内部类实现了 Runnable 接口,并提供了 run 方法的实现。

匿名内部类的一些特性:

  1. 匿名内部类没有类名,所以你不能使用 new 关键字创建它的实例(除了在创建它的地方)。
  2. 匿名内部类总是继承一个类或实现一个接口,但不能有 extendsimplements 子句。
  3. 匿名内部类只能创建一个实例。
  4. 由于匿名内部类没有类名,所以它不能有构造函数。

虽然匿名内部类提供了一种方便的方式来创建一次性的对象,但是它也有一些限制。例如,匿名内部类不能被重复使用,因为它没有类名,所以你不能在其他地方创建它的实例。此外,匿名内部类不能继承其他类(除了它实现的接口的父类或它继承的类)。

3.7 静态内部类

在Java中,静态内部类,也称为静态嵌套类,是定义在另一个类内部的静态类。与普通的内部类不同,静态内部类不持有其外部类的一个引用,因此它不能直接访问外部类的非静态成员。但是,静态内部类可以访问外部类的静态成员。

以下是一个静态内部类的例子:

public class OuterClass {
    private static String greeting = "Hello, world!";

    public static class StaticNestedClass {
        public void printGreeting() {
            System.out.println(greeting); // 静态内部类访问外部类的静态成员
        }
    }
}

你可以像下面这样创建一个静态内部类的实例:

OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
nestedObject.printGreeting(); // 输出 "Hello, world!"

静态内部类有几个常见的用途:

  1. 作为外部类的辅助类:如果一个类只被一个其他类使用,那么将它作为一个静态内部类可以使代码的结构更清晰。
  2. 封装静态成员:静态内部类可以访问外部类的静态成员,包括私有成员。这使得它可以用来封装外部类的静态成员。
  3. 定义更清晰的命名空间:静态内部类可以帮助我们更好地组织代码,创建一个更清晰的命名空间。

虽然静态内部类不能直接访问外部类的非静态成员,但是你可以通过传递一个外部类的实例给静态内部类(例如,作为一个构造函数的参数),让静态内部类能够间接地访问外部类的非静态成员。

4. 服务加载器

Java 的服务加载器(ServiceLoader)是 Java 提供的一种服务提供者加载设施,可以用于延迟实例化服务提供者,或者按需加载。

ServiceLoader 用于加载实现了某一特定接口的类。这些实现类的全类名需要在类路径下的 META-INF/services/ 目录中的一个以接口全名命名的文件中进行声明。

举个例子,假设你有一个接口叫做 com.example.MyService,你有两个类 com.example.MyServiceImpl1com.example.MyServiceImpl2 实现了这个接口。然后你在 META-INF/services/com.example.MyService 文件中写入:

com.example.MyServiceImpl1
com.example.MyServiceImpl2

然后,你可以使用 ServiceLoader 来动态加载这两个实现类:

ServiceLoader<MyService> services = ServiceLoader.load(MyService.class);
for (MyService service : services) {
    service.doSomething();
}

这种机制可以让你在编译时不需要知道具体的实现类,而在运行时动态加载实现类。这在创建可插拔或模块化的应用程序时非常有用。例如,Java 的 JDBC 驱动加载机制就是使用 ServiceLoader 来实现的。

需要注意的是,由于 ServiceLoader 是通过类加载器来加载服务实现的,所以要确保服务实现类是可以通过类加载器找到的,否则 ServiceLoader 将无法找到服务实现。通常,这意味着服务实现类需要包含在类路径中,或者在一个可由类加载器访问的 JAR 文件中。

5. 代理

5.1 何时使用代理

在Java编程中,代理模式可以在很多不同的场景中使用,以下是一些常见的使用代理的情况:

  1. 远程代理:在远程方法调用(Remote Method Invocation,RMI)中,一个对象在一台机器上调用另一台机器上对象的方法。在客户端机器上的对象就是一个代理,它代表了服务器机器上的实际对象。
  2. 虚拟代理:如果一个对象的创建和初始化非常耗时,你可能想要在需要的时候再创建它。虚拟代理可以在需要的时候创建实际的对象。
  3. 保护代理:如果一个对象有不同的访问权限,你可能想要在调用该对象的方法时进行检查。保护代理可以在调用实际对象的方法前检查调用者是否有足够的权限。
  4. 智能引用代理:当一个对象被引用时,你可能想要做一些额外的操作,比如计算对象的引用次数、锁定对象以防止其他对象修改它,或者在对象不再使用时自动释放它等。
  5. 日志代理:你可能想要在调用一个对象的方法前后做一些日志记录。日志代理可以在调用实际对象的方法前后添加日志代码。
  6. AOP(面向切面编程):AOP是一种编程技术,可以在不修改源码的情况下改变代码的行为。在Java中,AOP通常通过代理实现。代理在调用实际对象的方法前后插入"切面",以添加或修改行为。

需要注意的是,代理并不总是必要的。在没有明确的需求或好处的情况下,添加代理可能会导致代码不必要的复杂。而且,由于代理的方法调用通常涉及反射,所以可能会有性能的影响。在使用代理时,应该谨慎考虑是否真的需要它,以及它是否能带来足够的好处。

5.2 创建代理对象

在 Java 中,可以使用 java.lang.reflect.Proxy 类的 newProxyInstance 静态方法来创建代理对象。此方法需要接收三个参数:

  1. 一个类加载器,用于加载代理类。
  2. 一个接口数组,代理类需要实现的接口列表。
  3. 一个实现了 InvocationHandler 接口的对象,用于处理在代理对象上的方法调用。

下面是一个创建代理对象的例子:

import java.lang.reflect.*;

interface MyInterface {
    void myMethod();
}

class MyInvocationHandler implements InvocationHandler {
    private MyInterface myInterface;

    MyInvocationHandler(MyInterface myInterface) {
        this.myInterface = myInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method call");
        Object result = method.invoke(myInterface, args);
        System.out.println("After method call");
        return result;
    }
}

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterfaceImpl(); // MyInterfaceImpl 是 MyInterface 的一个实现

        MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
            MyInterface.class.getClassLoader(),
            new Class<?>[]{MyInterface.class},
            new MyInvocationHandler(myInterface)
        );

        proxy.myMethod(); // 调用方法时,会先调用 MyInvocationHandler 的 invoke 方法
    }
}

在这个例子中,MyInterface 是我们要创建代理的接口,MyInvocationHandler 是处理代理对象方法调用的处理器。我们通过 Proxy.newProxyInstance 方法创建了一个 MyInterface 的代理对象,并传入了一个 MyInvocationHandler 实例来处理方法调用。然后我们在代理对象上调用 myMethod 方法,调用前后会先后调用 MyInvocationHandlerinvoke 方法。

这种方式创建的是动态代理,也就是说我们在运行时动态创建了一个代理类,这个代理类实现了我们指定的接口,并将方法调用转发给 InvocationHandler。这种方式的优点是灵活,我们可以在运行时动态地创建任何接口的代理,并动态地处理方法调用。但是,因为这种方式使用了反射,所以可能会有性能的影响。

5.3 代理类的特性

Java中的代理类具有以下特性:

  1. 代理接口:代理类可以实现一个或多个接口。当你创建一个代理实例时,你需要提供一个接口数组,代理类将实现这些接口。
  2. 代理方法的调用处理:当你在代理对象上调用一个接口方法时,调用会被转发到一个调用处理器(InvocationHandler)。InvocationHandler 是一个接口,它定义了一个 invoke 方法,这个方法接收三个参数:代理对象、代表被调用方法的 Method 对象,以及一个包含方法参数的对象数组。
  3. 方法调用的转发:代理对象上的所有方法调用都会被转发到 InvocationHandler。在 InvocationHandlerinvoke 方法中,你可以添加任何逻辑,例如记录日志、修改参数、抛出异常、在调用前后执行其他代码,或者调用其他对象的方法。
  4. 代理类的动态生成:Java的代理类是在运行时动态生成的,你不需要预先创建代理类的源代码。这使得你可以在运行时动态创建任何接口的代理,并动态处理方法调用。
  5. 代理对象的类型:代理对象的类型是代理类,这个代理类是在运行时动态生成的。代理类继承了 java.lang.reflect.Proxy 类,并实现了你指定的接口。

需要注意的是,Java的动态代理只能对接口进行代理,不能对类进行代理。如果你需要对类进行代理,你可能需要使用第三方库,例如 CGLIB 或者 Javassist。或者,你也可以使用 Java 的字节码库(如 ASM)来手动创建代理类,但这通常需要更复杂的编程技术。