java-接口、lambda表达式

警告
本文最后更新于 2022-11-18,文中内容可能已过时,请谨慎使用。
注意
本文档属于个人笔记,个人水平有限,请酌情采纳,有任何错误可在评论区指出

接口是一个不含任何成员变量的抽象类

  • java8中接口中所有方法必须为public(实现该接口时,重写的方法必须声明为public)
  • 接口中不能定义成员变量,但是可以定义为一个常量(类型为public static final)
  • 不能用new来实例化一个接口,但是允许定义一个接口变量
  • 接口变量必须引用实现了该接口的类对象
  • 同一个类可以同时实现多个接口
  • 接口可以定义default方法,需要在方法名前添加default关键字,该方法提供了一个默认的实现,实现该接口时可以按需重写该方法
  • 接口中的默认方法可以调用其他任意方法
  • java8之后允许接口定义静态方法
  • 接口中的方法不能使用final关键字修饰
  • 当Java抽象类实现某个接口后没必要实现所有的方法。普通类必须实现所有抽象方法。
public class InterfaceTest implements Test{
    public static void main(String[] args) {
        InterfaceTest test = new InterfaceTest();
        // 调用接口的默认方法
        test.getMessage();
        System.out.println(InterfaceTest.info);
        // 调用接口的静态方法
        System.out.println(Test.staticMethod());
    }

    @Override
    public void getHint() {
        System.out.println("call getHint");
    }
}

interface Test {
    // 常量
    String info = "test interface";
    // 默认方法
    default void getMessage() {
        getHint();
        System.out.println("test message");
    }
    // 一般方法
    void getHint();
	// 静态方法
    static String staticMethod() {
        return "this is static method";
    }
}

1.超类优先:如果超类提供了一个具体方法, 同名而且有相同参数类型的默认方法会被忽略

假设Test接口的默认方法getMessageTest1类的方法getMessage重名

只实现Test接口时不需要重写默认方法getMessage

public class InterfaceTest implements Test{
    public static void main(String[] args) {
        InterfaceTest test = new InterfaceTest();
        // 调用接口的默认方法
        test.getMessage();
        System.out.println(InterfaceTest.info);
        // 调用接口的静态方法
        System.out.println(Test.staticMethod());
        System.out.println();
    }
    @Override
    public void getHint() {
    }
}

interface Test {
    // 常量
    String info = "test interface";
    // 默认方法
    default void getMessage() {
        getHint();
        System.out.println("Test: message");
    }
    // 一般方法
    void getHint();

    static String staticMethod() {
        return "Test: this is static method";
    }
}

class Test1 {
    void getMessage() {
        System.out.println("Test1: message");
    }
}

如果InterfaceTest同时继承了Test1类并实现了Test接口,那么从Test接口的默认方法会被忽略,必须进行重写

public class InterfaceTest extends Test1 implements Test{
    public static void main(String[] args) {
        InterfaceTest test = new InterfaceTest();
        // 调用接口的默认方法
        test.getMessage();
        System.out.println(InterfaceTest.info);
        // 调用接口的静态方法
        System.out.println(Test.staticMethod());
        System.out.println();
    }

    @Override
    public void getMessage() {
        Test.super.getMessage();
    }

    @Override
    public void getHint() {
    }
}

interface Test {
    // 常量
    String info = "test interface";
    // 默认方法
    default void getMessage() {
        getHint();
        System.out.println("Test: message");
    }
    // 一般方法
    void getHint();

    static String staticMethod() {
        return "Test: this is static method";
    }
}

class Test1 {
    void getMessage() {
        System.out.println("Test1: message");
    }
}

2.接口冲突:如果一个类同时实现了多个接口,并且这些接口中包含相同的默认方法

实现类必须重写重名方法,手动选择使用某个接口的默认方法

这里假设同时实现了TestTest1接口,两个接口都有getMessage()

public class InterfaceTest implements Test, Test1 {
    public static void main(String[] args) {
        InterfaceTest test = new InterfaceTest();
        // 调用接口的默认方法
        test.getMessage();
    }

    @Override
    public void getMessage() {
        // 使用Test1的getMessage
        // Test1.super.getMessage();
        // 或者使用Test的getMessage
        Test.super.getMessage();
    }


    @Override
    public void getHint() {
    }
}

interface Test {
    // 常量
    String info = "test interface";
    // 默认方法
    default void getMessage() {
        getHint();
        System.out.println("Test: message");
    }
    // 一般方法
    void getHint();

    static String staticMethod() {
        return "Test: this is static method";
    }
}

interface Test1 {
    default void getMessage() {
        System.out.println("Test1: message");
    }
}

有这样一种情况,此时Test1接口的getMessage不是默认方法,InterfaceTest并不会默认使用Test的默认方法。必须重写getMessage方法(可以指定使用 Test 的默认方法)

也就是说: 如果至少有一个接口提供了一个同名的默认实现, 编译器就会报告错误, 而程序员就必须解决这个二义性。

public class InterfaceTest implements Test, Test1 {
    public static void main(String[] args) {
        InterfaceTest test = new InterfaceTest();
        // 调用接口的默认方法
        test.getMessage();
    }

    @Override
    public void getMessage() {
        Test.super.getMessage();
    }

    @Override
    public void getHint() {

    }
}

interface Test {
    // 常量
    String info = "test interface";
    // 默认方法
    default void getMessage() {
        getHint();
        System.out.println("Test: message");
    }
    // 一般方法
    void getHint();

    static String staticMethod() {
        return "Test: this is static method";
    }
}

interface Test1 {
    void getMessage();
}

String类实现了Comparable<String>接口,其中的String.compareTo()实现了按字典序排序

需求: 将字符串按照长度从小到大排序,如果长度相等,则按字典序排序

这里需要重新实现comparator接口的compare方法

public  class InterfaceTest {
    public static void main(String[] args) {
        String[] strs = {"Happy", "new", "year"};

        Comparator<String> cmp = new LengthComparator();

        for (int i = 0; i < strs.length - 1; i++) {
            if (cmp.compare(strs[i],strs[i+1]) > 0) {
                String tmp = strs[i];
                strs[i]=strs[i+1];
                strs[i+1] = tmp;
            }
        }

        System.out.println(Arrays.toString(strs));
    }
}

class LengthComparator implements Comparator<String> {
    // 返回正值的时候说明需要交换位置
    @Override
    public int compare(String o1, String o2) {
        // 长度相等,按字典序排序
        if (o1.length() == o2.length()) {
            return o1.compareTo(o2);
        }
        // 按长度从小到大排序
        return o1.length() - o2.length();
    }
}

简写方式:

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

public  class InterfaceTest {
    public static void main(String[] args) {
        String[] strs = {"Happy", "new", "year"};

        Arrays.sort(strs, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                if (o1.length() == o2.length()) {
                    return o1.compareTo(o2);
                }
                return o1.length() - o2.length();
            }
        });

        System.out.println(Arrays.toString(strs));
    }
}

Object.clone()默认的克隆操作是浅拷贝,并没有克隆对象中引用的其他对象。 浅拷贝会有什么影响吗?如果原对象和浅克隆对象共享的子对象是不可变的(如 String 类),那么这种共享就是安全的。或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。

下面的图给出了一个浅拷贝的例子,Date是一个可变类

/images/all/image-20221117000319512.png
注意
一般而言,子对象都是可变的,所以需要重写clone方法来实现对象的深拷贝

要使用Object.clone()方法(浅拷贝)或者重写来实现深拷贝方法,都必须完成下面的两步:

  • 让所在类实现Cloneable接口
  • clone()方法的权限改为public(这样这个方法在所有类中都可以调用)
public class Main  {
    public static void main(String[]args) throws CloneNotSupportedException {
        Test t = new Test();
        Test other = (Test) t.clone();
        System.out.println(t.hashCode());
        System.out.println(other.hashCode());
    }
}

class Test implements Cloneable{

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

让图6.2的实现深拷贝

package CloneTest;

import java.util.Date;
import java.util.GregorianCalendar;

public class CloneTest
{
    public static void main(String[] args)
    {
        try
        {
            Employee original = new Employee("John Q. Public", 50000);
            original.setHireDay(2000, 1, 1);
            Employee copy = original.clone();
            copy.raiseSalary(100);
            copy.setHireDay(2022, 11, 17);
            System.out.println("original=" + original);
            System.out.println("copy=" + copy);

        }
        catch (CloneNotSupportedException e)
        {
            e.printStackTrace();
        }
    }
}

class Employee implements Cloneable
{

    private String name;
    private double salary;
    private Date hireDay;
    public Employee(String n, double s)
    {
        name = n;
        salary = s;
        hireDay = new Date();
    }

    public Employee clone() throws CloneNotSupportedException
    {
        // 先调用一次浅拷贝
        // Object.clone()返回的是Object对象
        // call Object.clone()
        Employee cloned = (Employee) super.clone();

        // 对可变对象进行克隆
        // clone mutable fields
        cloned.hireDay = (Date) hireDay.clone();

        return cloned;
    }

    public void setHireDay(int year, int month, int day)
    {
        Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();

        // Example of instance field mutation
        hireDay.setTime(newHireDay.getTime());
    }

    public void raiseSalary(double byPercent)
    {
        double raise = salary * byPercent / 100;
        salary += raise;
    }

    public String toString()
    {
        return "Employee[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]";
    }

}

protected修饰clone方法,主要是为了让子类去重写它,实现深拷贝,以防在其他任何地方随意调用后修改了对象的属性对原来的对象造成影响。

lambda表达式形式:(参数)-> 一个表达式或{代码块}

  • 无需指定表达式的返回类型,可通过上下文进行推断
  • 代码块需要显式的指定返回值
  • 即使没有参数也要提供一个空括号()
  • 如果可以推导出lambda表达式的参数类型,可以忽略其类型
package LambdaTest;

import java.util.*;

public class LambdaTest {
    public static void main(String[] args) {
        Runnable hello = () -> {
            System.out.println("hello");
        };
        hello.run();

        String[] strs = new String[] {"Hello","Year","New"};
        // 按字符串长度升序排序
        Comparator<String> stringComparator = (first, second) -> {
            if (first.length() < second.length()) return -1;
            else if (first.length() > second.length()) return 1;
            else return 0;
        };
        Arrays.sort(strs,stringComparator);
        System.out.println(Arrays.toString(strs));

        // 按字符串长度降序排序
        Comparator<String> stringComparator1 = (first,second) -> second.length() - first.length();
        Arrays.sort(strs,stringComparator1);

        System.out.println(Arrays.toString(strs));
    }
}
  • 函数式接口只有一个抽象方法
  • 接口重写了 Object 的公共方法也不算入内
技巧
Comparator就是一个函数式接口,接口之前有@FunctionalInterface注解

最好把 lambda 表达式看作是一个函数而不是一个对象,另外lambda表达式可以传递到函数式接口。

注意
Object不是一个函数式接口,不能将lambda表达式赋给类型为Object变量

Predicate函数式接口的主要作用就是提供一个test方法,接受一个参数返回一个布尔类型,Predicate接口在stream api中进行一些判断的时候非常常用。

在 Java 8 中,我们会使用lambda表达式创建匿名方法,但是有时候,我们的lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8 的方法引用允许我们这样做。方法引用是一个更加紧凑,易读的lambda表达式,注意方法引用是一个lambda表达式,其中方法引用的操作符是双冒号"::"

lamda 表达式的代码可以改写为方法引用的形式

public class LambdaTest {
    public static void main(String[] args) {
        Runnable hello = () -> {
            System.out.println("hello");
        };
        hello.run();

        String[] strs = new String[] {"Hello","Year","New"};
        // 按字符串长度升序排序
        Arrays.sort(strs, LambdaTest::compare2);
        System.out.println(Arrays.toString(strs));

        // 按字符串长度降序排序
        Arrays.sort(strs, LambdaTest::compare);

        System.out.println(Arrays.toString(strs));
    }

    private static int compare(String first, String second) {
        return second.length() - first.length();
    }

    private static int compare2(String first, String second) {
        if (first.length() < second.length()) return -1;
        else if (first.length() > second.length()) return 1;
        else return 0;
    }

方法引用主要有以下三种形式:

  1. object::instanceMethod 对象 + 实例方法
  2. Class::staticMethod 类名+ 静态方法
  3. Class::instanceMethod 类名 + 实例方法
// 第一种
System.out::println // 等价于x -> System.out.println(x)

// 第二种
Math::pow // 等价于(x,y)-> Math.pow(x,y)

// 第三种,第一个参数将作为object
String::compareToIgnoreCase // 等价于(x, y) -> x.compareToIgnoreCase(y)

对于第三种方法,显然不能用类调用实例方法,这样编译都会报错!

当一个对象调用一个方法,方法的参数中包含一个函数式接口,该函数式接口的第一个参数类型是这个对象的类,那么这个函数式接口可用方法引用代替,并且替换用的方法可以不包含函数式接口的第一个参数

@FunctionalInterface
interface TestInterface<T> {
    String handleString(T a, String b);
}
class TestClass {
    private final String oneString;

    TestClass(String oneString) {
        this.oneString = oneString;
    }

    public String concatString(String a) {
        return this.oneString + a;
    }
    public String startHandleString(TestInterface<TestClass> testInterface, String str) {
        return testInterface.handleString(this, str);
    }
}

public class Test {
    public static void main(String[] args) {
        TestClass testClass = new TestClass("abc");
        String result = testClass.startHandleString(TestClass::concatString, "123");
        System.out.println(result);

        //相当于以下效果
        TestClass testClass2 = new TestClass("abc");
        TestInterface testInterface = (a, b) -> testClass2.concatString(b);
        String result2 = testInterface.handleString(testClass2, "123");
        System.out.println(result2);

    }
}

1.构造器引用

语法形式:className::new

构造函数本质上是静态方法,只是方法名字比较特殊,使用的是new 关键字

public class ConstructorRefDemo {

    public static void main(String[] args) {
        // 利用MyClass的构造方法实现了MyFunc接口
        MyInterface myInterface = MyClass :: new;
        MyClass mc = myInterface.func(100);
        System.out.println("val in mc is: " + mc.getValue());
    }
}

interface MyInterface {
    MyClass func(int n);
}

class MyClass {
    private final int val;

    MyClass(int v) {
        val = v;
    }

    public int getValue() {
        return val;
    }
}

还有一个更难理解的例子

package interfaceTest;

import java.util.Arrays;
import java.util.List;

@FunctionalInterface
interface Supplier<T>
{
	T get();
}

class Car
{
	public static Car create(final Supplier<Car> supplier)
	{
		return supplier.get();
	}

	public static void collide(final Car car)
	{
		System.out.println("静态方法形式调用 " + car.toString());
	}

	public void follow(final Car another)
	{
		System.out.println("对象方法形式调用 " + another.toString());
	}

	public void repair()
	{
		System.out.println("任意对象方法引用 " + this.toString());
	}

	@Override
	public String toString()
	{
		return "just a Car " + this.hashCode();
	}
}

public class CRacer
{
	public static void main(String[] args)
	{
		//构造器引用:它的语法是Class::new
		Car car0 = Car.create(Car::new);
		Car car1 = Car.create(Car::new);
		System.out.println(car0.hashCode());
		System.out.println(car1.hashCode());
		final List<Car> cars = Arrays.asList(car0,car1);

		//静态方法引用:Class::static_method
		cars.forEach(Car::collide);

		//特定类的任意对象的方法引用Class::method
		cars.forEach(Car::repair);
		cars.forEach(System.out::println);


		final Car police = Car.create(Car::new);
		System.out.println(police.hashCode());
		cars.forEach(police::follow);
	}
}

2.构造数组引用

语法形式:Typename[]::new

import java.util.Arrays;
import java.util.function.Function;

public  class InterfaceTest {
    public static void main(String[] args) {
        Function<Integer, String[]> func1 = length -> new String[length];
        String[] arr1 = func1.apply(5);
        for (int i = 0; i < arr1.length; i++) {
            arr1[i] = "arr1_" + i;
        }
        System.out.println(Arrays.toString(arr1));



        Function<Integer, String[]> func2 = String[]::new;
        String[] arr2 = func2.apply(10);
        for (int i = 0; i < arr2.length; i++) {
            arr2[i] = "arr2 _" + i;
        }
        System.out.println(Arrays.toString(arr2));
    }
}

自由变量的值,这是指非参数而且不在代码中定义的变量。

package interfaceTest;

import javax.swing.*;
import java.awt.event.ActionListener;

public  class InterfaceTest {
    public static void main(String[] args) throws InterruptedException {
        countDown("Hello",1000);
        Thread.sleep(2000);
        System.exit(0);
    }
    public static void countDown(String message,int delay) {
        ActionListener listener = event -> {
            System.out.println(message);
        };
        new Timer(delay,listener).start();
    }
}

比如countDown中的message就是自由变量,也可以说messagelambda表达式捕获的。

注意:lambda表达式中只能引用值不会改变的变量。因为在lambda表达式中改变变量,并发执行多个操作时可能会不安全

lambda表达式捕获的变量也称为最终变量,也就是该变量在初始化后不会被赋新值。

在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。this关键字的含义不会随lambda表达式而改变。

import javax.swing.*;
import java.awt.event.ActionListener;

public  class InterfaceTest {
    public static void main(String[] args) throws InterruptedException {
        new Application().init();
        Thread.sleep(2000);
        System.exit(0);
    }
}

class Application {
    public void init() {
        ActionListener listener = e -> {
            // 调用的是Application的toString()方法
            System.out.println(this.toString());
        };
        new Timer(1000,listener).start();
    }

    @Override
    public String toString() {
        return "hello";
    }
}

Runnable: 无参数,返回值类型为void,抽象方法名为run

IntConsumer: 接受参数类型为int,返回值类型为void,抽象方法名为accept

import java.util.function.IntConsumer;

public  class InterfaceTest {
    public static void main(String[] args)  {
        repeat(10,() -> System.out.println("hello"));
        repeatAndNumber(10,i -> System.out.println("hello_" + i));
    }

    public static void repeat(int n, Runnable runnable) {
        for (int i = 0; i < n; i++) {
            runnable.run();
        }
    }

    public static void repeatAndNumber(int n, IntConsumer consumer) {
        for (int i = 0; i < n; i++) {
            consumer.accept(i);
        }
    }
}

相关文章