接口和lambda表达式

接口

几口不是类,是对类的一组需求描述,类负责实现接口。

接口中所有的方法自动属于public,因此在接口中声明方法时,不需要提供public. 接口只负责定义方法,不能有实例域。 Java SE 8 前,接口不能实现方法,8及以后可以定义默认方法。

语法

Java中使用interface关键字定义接口


public intface TestInterface

{

}


类实现接口,使用implements关键字


Class Test implements TestInterface
{
	...
}

接口中方法自动为public,接口中的域自动设置为public static final

一个类可以实现多个接口,接口可以继承多个接口。

Class Test implements TestInterface,TestInterface2
{
	...
}

Comparable 接口

Comparable是一个泛型接口,实现这个接口要提供泛型参数。

用于两个对象的比较.

Arrays.sort()中用来比较实现了Comparable的 类 的数组

IntegerDouble含有静态方法compare,用于比较相应类型的两个基本类型数值。

静态方法

Java 8中,运行在接口中添加静态方法。

但通常的做法是将静态方法放到伴随类中,例如Collection/Collections,Path/Paths

例如在接口中定义静态方法。

public interface Path {
    static java.nio.file.Path get(String first, String... more) {
        return FileSystems.getDefault().getPath(first, more);
    }
}

默认方法

可以给接口提供一个默认实现,必须用default修饰符修饰

public interface Path {
    static java.nio.file.Path get(String first, String... more) {
        return FileSystems.getDefault().getPath(first, more);
    }

    default java.nio.file.Path get(String path) {
        return get(path, null);
    }
}

默认方法冲突

如果接口中将一个方法定义为默认方法,又在超类或另一个类定义了同样的方法,Java根据以下规程处理:

  1. 超类优先,如果超类提供了一个具体方法,接口中的默认方法会被忽略。

  2. 接口冲突,如果多个超接口提供了同名同参的方法,则程序员必须手动覆盖这个方法解决这个冲突

    interface Named{
        default String getName(){
            return getClass.getName();
        }
    }
    interface Person{
        default String getName(){
            return getClass.getName();
        }
    }
    
    class Student implements Person,Named{
        ...
    }
    

Student必须重新实现getName()方法,如果要调用Named接口的默认方法

public class Student implements Named, Person {

    @Override
    public String getName() {
        return Named.super.getName();
    }
}

不要在接口中定义Object类中方法的默认方法,例如不能为toString``equals定义默认方法,由于类优先规则,这类方法无法超越Object.toString()Object.equals

接口实例

回调接口

回调(callback)是一种程序设计默认,在某个特定事件发生时调用我们指定的动作。例如按下鼠标,或接受到某个事件系统应该做什么。

java.swing中有一个Timer类,它可以在到达指定时间间隔后发出通知,并调用我们传入的回调方法。

start方法开启。

public class TestTimer {
    public static void main(String[] args) {

        Timer timer = new Timer(1000, e -> {
            System.out.printf("%1$ta %1$tb %1$td %1$tT %1$tZ %1$tY \n", new Date());
        });
        timer.start();
        JOptionPane.showMessageDialog(null, "Quit program");
        System.exit(0);
    }
}

Comparator

String 类实现了Comparable<String>接口,实现是根据字典顺序排序。如果我们要使用自定义排序方式,不可能继承String,我们可以使用Arrays.sort的另一个版本,一个数组和一个比较器compartor

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

实例

package top.himcs.basic.inter;

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

public class TestComparator {
    public static void main(String[] args) {
        Comparator<String> stringComparable = new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length() - o2.length();
            }
        };
        String s1 = "33123";
        String s2 = "1333";
        String[] ss = new String[]{s1, s2};
        Arrays.sort(ss, stringComparable);
        System.out.println(Arrays.toString(ss));
    }
}

对象克隆

cloneObjectprocted方法,默认的克隆方式是浅拷贝,即不会克隆对象中的引用对象,只会克隆基本类型。

克隆前三问?

  1. 默认的clone方法是否满足要求

  2. 是否在子对象上使用克隆

  3. 是否不应该使用克隆

    优先考虑不使用克隆,如果使用克隆,类必须:

    1. 实现Cloneable接口

    2. 重新定义clone方法

Cloneable 接口是Java 提供的一组标记接口( tagging interface) 之一。(有些程序员称之为记号接口( marker interface))。应该记得,Comparable 等接口的通常用途是确保一个类实现一个或一组特定的方法。标记接口不包含任何方法; 它唯一的作用就是允许在类型查询中使用instanceof:

if(obj instanceof Cloneable)...

数组也可使使用clone()规则与类克隆一直,基本类型是值克隆,互不影响,类还是会引用同一份对象。

    public static void main(String[] args) {
        String[] strings = new String[]{"a", "b", "c"};
        String[] strings2 = strings.clone();
        strings[0] = "aa";
        System.out.println(Arrays.toString(strings2));
        System.out.println(strings);
        System.out.println(strings[1].hashCode());
        System.out.println(strings2[1].hashCode());
        Employee[] employees = new Employee[]{ new Employee()};
        Employee[] employees2 = employees.clone();
        System.out.println(employees[0].hashCode());
        System.out.println(employees2[0].hashCode());
        System.out.println(employees[0] == employees2[0]);
    }

lambda 表达式

Java 8 引入,用简洁的语法定义一个函数接口。

javascript中的箭头函数类似()=>{}

语法

Java中 用->表示lamdba表达式

(String frist,String second)->{
	return first+second;
}

如果没有参数,要提供一个空括号

()->{return ''}

如果可以推到出参数类型,类型参数也可以省略

(first,second)->{
	return first+second;
}

如果只有一个参数,小括号也可以省略

first->{}

如果表达式函数只有一行,可以无需写return也可以不加括号

(first,second)->first+second;

函数式接口

对于只有一个抽象方法的接口,当需要这种接口的对象时,可以提供太一个lambda表达式。这种接口成为函数式接口functional interface.

接口可以提供非抽象方法,例如提供默认方法,静态方法,或者重新声明Object类对象的方法,都不会影响唯一的抽象方法,这个接口依然是函数式接口。

例如Compartor接口是一个函数式接口

可以如下调用:

	        Arrays.sort(employees, (employee1, employee2) -> {
            return employee1.hashCode() - employee2.hashCode();
        });

Java会将声明的lambda表达式转换为实际的函数接口。

直接声明一个函数式接口

        Comparator comparator = (Comparator<String>) (String s1, String s2) -> {
            return s1.length() - s2.length();
        };

方法引用

将一个方法传递给需要所需函数式接口的地方,如下所示。

语法

        Timer timer1 = new Timer(1000, System.out::println);
        timer1.start();

等价于

event->{System.out.println(event)}

使用::操作符有以下三种情况:

  1. object::instanceMethod
  2. Class::staticMethod
  3. Class::instanceMethod

前两种情况,等价于提供方法参数的lambda,即将参数传入方法。 例如System.out::println等价于x->System.out.println(x)Math::pow等价于(x,y)->Math.pow(x,y) 第三种情况下,第一个参数会成为方法的调用者。例如String::compareToIgnoreCase等价于(x,y)->x.compareToIgnoreCase(y)

如果有多个同名的重载方法,编译器会尝试从上下文中寻找需要的方法。

类似于lambda表达式,方法引用不能独立存在,总是会转化为函数式接口的实例。

可以在方法中使用this参数,例如this::equals等价于x->this.equals(x)

,使用super也是合法的。

例如super::toString

package top.himcs.basic.inter;

import java.util.Arrays;

public class TestLambda {
    public static void main(String[] args) {
        TestLambda.test(TestLambda::printV2, new Object[]{"Hello", "World"});
    }

    public static void test(test t, Object... objects) {
        t.print(objects);
    }

    public static void printV2(Object... objects) {
        System.out.println(Arrays.toString(objects));
    }

    @FunctionalInterface
    interface test {
        void print(Object... objects);
    }
}

存储函数表达式的接口

java.util.function下定义了很多通用的函数式接口。

例如BiFunction<T, U, R>接受参数类型TU,返回类型为R的函数,可以将我们的函数保存到这中类型的变量中。

        BiFunction<String, String, Integer> function = (s1, s2) -> {
            return s1.length() - s2.length();
        };

Predicate<T>是一个保存参数类型为T,返回结果为boolean的j函数接口,可以用来测试

例如

        Predicate<String> predicate = s -> {
            return s.isEmpty();
        };

ArrayList有个removeIf方法,参数为一个Predicate

常用函数式表达式接口

接口参数返回方法名描述
Runnablevoidrun作为无参数或返回值的动作运行
Supplier<T>Tget返回一个T类型的值
Consumer<T>Tvoidaccept处理一个T类型的值
BiConsumer<T, U>T, Uvoidaccept处理T和U类型
BiFunction<T, U, R>T,URapply处理T和U类型,返回一个R类型
Function<T, R>TRapply处理T类型,返回一个R类型
UnaryOperator<T> extends Function<T, T>TTapplyT类型的一元操作
BinaryOperator<T> extends BiFunction<T,T,T>T,TTapplyT类型的二元操作
Predicate<T>Tbooleantest测试T的布尔值
BiPredicate<T, U>T,Ubooleantest两个参数的布尔值

由于泛型只能为Object引用类型,基础类型要最好用专门的标准接口,这样可以减少装箱操作提高性能,例如处理int接口

@FunctionalInterface
public interface IntConsumer {
      void accept(int value);
}

如果设计你自己的接口,其中只有一个抽象方法, 可以用@FunctionalInterface 注解来标记这个接口。

这样做有两个优点。

如果你无意中增加了另一个非抽象方法, 编译器会产生一个错误消息。

另外javadoc 页里会指出你的接口是一个函数式接口。 并不是必须使用注解根据定义, 任何有一个抽象方法的接口都是函数式接口。不过使用@FunctionalInterface 注解确实是一个很好的做法。

构造器引用

构造器引用与方法引用类似,方法名为new例如Person::newPerson构造器的一个引用,具体调用哪个构造器,取决于上下文.

例如

ArrayList<String> names = ...;
Stream<Person> stream == names.streams().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

数组也可以建立构造器引用,例如int[]::new,有一个参数,即数组的长度

等价于x->new int[x]

Java有一个验证,无法构造泛型T的数组,表达式new T[n]会报错,并会改为new Object[n].

获取一个Person引用数组

Person[] people = stream.toArray(Person[]::new)

下面这种方式不可以:

T[] ts = Arrays.asList(t1).stream().toArray(T[]::new);

只能通过数组反射来构造泛型数组。

如下

    public static <T> T[] get(Class<T> type,int length) {
        T[] ts = (T[])Array.newInstance(type, length);
        return ts;
    }

变量作用域

lamdba表达式中可以直接访问外部的变量。

可以理解为lamdba表达式转换为包含方法的一个对象,外部变量的值会复制到这个对象的实例变量中。

lambda中,只能引用值不会改变的变量。

lamdba中变量名不能和外部已有变量名的突。

lamdba中的this是值创建这个lamdba表达式的外部对象。

例如

public class Appliction(){
    public void int(){
        ActionListener listener = event -> {
            System.out.println(this.toString());
        }
    }
}

这里的this.toString()会调用Appliction对象的toString方法。

处理lambda表达式

lambda的精髓在于延迟执行deferred exection,如果要立即执行,可以直接执行,无需包装在lamdba表达式中。

可能要用到lamdba的原因:

  1. 单独线程执行
  2. 多次运行
  3. 算法的适当位置(例如排序中的比较操作)
  4. 事件回调(按钮点击,事件触发)
  5. 必要时运行的代码
Last Updated:
Contributors: himcs, himcs