JAVA

浅谈对Java双冒号::的理解

双冒号“::”就是 Java 中的方法引用(Method references)


方法引用的格式是类名::方法名。一般是用作Lambda表达式。

通过方法引用,可以将方法的引用赋值给一个变量,通过赋值给Function,说明方法引用也是一种函数式接口的书写方式,Lambda表达式也是一种函数式接口,Lambda表达式一般用于自己提供方法体,而方法引用一般直接引用现成的方法。

java8允许我们使用lambda表达式创建匿名方法。但有时lambda表达式除了调用现有方法之外什么也不做。在这些情况下,通过名称引用现有的方法,通常能更直白的表现出方法的调用过程。对于已经存在的且具有方法名称的方法,它其实是简洁且易于读取的一种lambda表达式,或者说是对lambda表达式的一种进一步简化。

public class User {  
    private String username;  
    private Integer age;  
  
    public User() {  
    }  
  
    public User(String username, Integer age) {  
        this.username = username;  
        this.age = age;  
    }  
  
    @Override  
    public String toString() {  
        return "User{" +  
                "username='" + username + '\'' +  
                ", age=" + age +  
                '}';  
    }  
  
    // Getter&Setter  
}  


public static void main(String[] args) {  
    // 使用双冒号::来构造静态函数引用  
    Function<String, Integer> fun = Integer::parseInt;  
    Integer value = fun.apply("123");  
    System.out.println(value);  
  
    // 使用双冒号::来构造非静态函数引用  
    String content = "Hello JDK8";  
    Function<Integer, String> func = content::substring;  
    String result = func.apply(1);  
    System.out.println(result);  
  
    // 构造函数引用  
    BiFunction<String, Integer, User> biFunction = User::new;  
    User user = biFunction.apply("mengday", 28);  
    System.out.println(user.toString());  
  
    // 函数引用也是一种函数式接口,所以也可以将函数引用作为方法的参数  
    sayHello(String::toUpperCase, "hello");  
}  

// 方法有两个参数,一个是  
private static void sayHello(Function<String, String> func, String parameter){  
    String result = func.apply(parameter);  
    System.out.println(result);  
}

方法引用的格式为<ClassName | instance>::<MethodName>。也就是被引用的方法所属的类名和方法名用双冒号::隔开,构造器方法是个例外,引用会用到new关键字,总结了一下:

Java 方法引用Java 8随着Lambda表达式引入的新特性。 可以直接引用已有Java类或对象的方法或构造器。方法引用通常与Lambda表达式结合使用以简化代码。其使用条件是:Lambda表达式的主体仅包含一个表达式,且Lambda表达式只调用了一个已经存在的方法;被引用的方法的参数列表和返回值与Lambda表达式的输入输出一致

形如 ClassName::methodName 或者 objectName::methodName表达式,叫做方法引用(Method Reference)。看看编译器是如何根据 “晦涩难懂” 的 Method Reference 来推断开发者的意图的。例如:

1.表达式:
person -> person.getName();
可以替换成:
Person::getName

2.表达式:
() -> new HashMap<>();
可以替换成:
HashMap::new

官方文档中将双冒号的用法分为了以下4类:

双冒号的作用


在使用双冒号前我们要先搞清楚一个问题:为什么要使用双冒号?也就是双冒号的作用是什么。

双冒号的设计初衷是为了化简Lambda表达式,不熟悉Lambda表达式的同学可以先了解一下。

Lambda表达式的形式有两种:

  • 包含单独表达式 :parameters -> an expression
    list.forEach(item -> System.out.println(item));

  • 包含代码块:parameters -> { expressions }
    list.forEach(item -> {
         int numA = item.getNumA();
         int numB = item.getNumB();
         System.out.println(numA + numB);
    });

使用双冒号可以省略第一种Lambda表达式中的参数部分,即item ->和调用方法的参数这两部分。

例如:

    //不使用双冒号
    list.forEach(item -> System.out.println(item));
    //使用双冒号
    list.forEach(System.out::println);

双冒号的使用条件


使用双冒号有两个条件:

  • 条件1:条件1为必要条件,必须要满足这个条件才能使用双冒号。Lambda表达式内部只有一条表达式(第一种Lambda表达式),并且这个表达式只是调用已经存在的方法,不做其他的操作。
  • 条件2:由于双冒号是为了省略item ->这一部分,所以条件2是需要满足不需要写参数item也知道如何使用item的情况。

有两种情况可以满足这个要求,这就是我将双冒号的使用分为2类的依据。

一些例子:


Lambda表达式的参数与调用函数的参数完全一致时

静态方法调用

    //化简前
    list.forEach(item -> System.out.println(item));
    //化简后
    list.forEach(System.out::println);

非静态方法调用

    StringBuilder stringBuilder = new StringBuilder();
    //化简前
    IntStream.range(1, 101).forEach(item -> stringBuilder.append(item));
    //化简后
    IntStream.range(1, 101).forEach(stringBuilder::append);

调用构造方法

先定义一个方法,这个方法的作用是将一个集合的内容复制到另一个集合

    public <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(SOURCE sourceCollection, Supplier<DEST> collectionFactory) {
         DEST result = collectionFactory.get();
         result.addAll(sourceCollection);
         return result;
    }

调用这个方法

    //化简前
    Set<Person> rosterSetLambda = transferElements(roster, () -> new HashSet<>());
    //化简后
    Set<Person> rosterSet = transferElements(roster, HashSet::new);

自己写的一个例子


第一个类:

@Data
public class ModelA {
     private String id;
 
     public ModelA(String id) {
     this.id = id;
     }
 
     public ModelA() {
     }
}
class ClassB {
     private final List<ModelA> list = new ArrayList<>();
 
     public void add(String string, Function<String, ModelA> function) {
     list.add(function.apply(string));
     }
}

测试代码

    ClassB classB = new ClassB();
    //化简前
    classB.add("ddd", item -> new ModelA(item));
    //化简后
    classB.add("ddd", ModelA::new);

另一个例子


JDK8中有双冒号的用法,就是把方法当做参数传到stream内部,使stream的每个元素都传入到该方法里面执行一下。

代码其实很简单,以前的代码一般是如此的:

public class AcceptMethod {
 
    public static void  printValur(String str){
        System.out.println("print value : "+str);
    }
 
    public static void main(String[] args) {
        List<String> al = Arrays.asList("a","b","c","d");
        for (String a: al) {
            AcceptMethod.printValur(a);
        }
      //下面的for each循环和上面的循环是等价的 
        al.forEach(x->{
            AcceptMethod.printValur(x);
        });
    }
}

现在JDK双冒号是:

public class MyTest {
    public static void  printValur(String str){
        System.out.println("print value : "+str);
    }
 
    public static void main(String[] args) {
        List<String> al = Arrays.asList("a", "b", "c", "d");
        al.forEach(AcceptMethod::printValur);
        //下面的方法和上面等价的
        Consumer<String> methodParam = AcceptMethod::printValur; //方法参数
        al.forEach(x -> methodParam.accept(x));//方法执行accept
    }
}

上面的所有方法执行玩的结果都是如下:

print value : a
print value : b
print value : c
print value : d

JDK源码如下:

/**
     * Performs the given action for each element of the {@code Iterable}
     * until all elements have been processed or the action throws an
     * exception.  Unless otherwise specified by the implementing class,
     * actions are performed in the order of iteration (if an iteration order
     * is specified).  Exceptions thrown by the action are relayed to the
     * caller.
     *
     * @implSpec
     * <p>The default implementation behaves as if:
     * <pre>{@code
     *     for (T t : this)
     *         action.accept(t);
     * }</pre>
     *
     * @param action The action to be performed for each element
     * @throws NullPointerException if the specified action is null
     * @since 1.8
     */
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

附:知乎的一个例子(比较好的例子)


双冒号::在JDK8的Lambda表达式函数中开始使用,用作方法引用。

具体用法,咱们来举个例子:

假设有个Person类:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }
    
    public Calendar getBirthday() {
        return birthday;
    }    

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }}

假设你的社交网络应用程序的成员包含在一个数组中,并且你希望按年龄对数组进行排序。你可以使用以下代码:

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
        
Arrays.sort(rosterAsArray, new PersonAgeComparator());

上面最后一行代码:Arrays.sort中的sort方法源码如下:

static <T> void sort(T[] a, Comparator<? super T> c)

请注意,此时的接口Comparator是一个用 @Functional 来注解的,因此,你可以使用lambda表达式,而不是定义并创建实现comparator的类的新实例,所以你可以用一个Lambda表达式来写方法定义,来创建Comparator的实例:

Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
        return a.getBirthday().compareTo(b.getBirthday());
    }
);

但是,我们已经在Person bean对象中提前写好了用来比较两个person对象的出生日期的方法:

所以你其实可以在lambda表达式的主体中直接调用这个方法方法,在Person.compareByAge函数中,其instance已存在,所以我们要比较两个Person实例的年龄大小,用Lambda expression可以这样写:

Arrays.sort(rosterAsArray,
  (Person a, Person b) -> {
    return a.getBirthday().compareTo(b.getBirthday());
  }
);

因为这个上面这个lambda表达式是在调用现有的方法,所以我们这里就可以使用上面提到的使用方法引用方式(及双冒号 ::),而不是之前我们熟悉的lambda表达式,由于咱们的Lambda表达式调用的是一个已经存在的方法,即,我们做的就是方法引用,所以你就可以用双冒号::来改造一下:

Arrays.sort(rosterAsArray, Person::compareByAge);

方法引用 person::comparebyage 在语义上与lambda表达式(a,b)->person.comparebyage(a,b)相同。他们都有以下特点:

  • 它的形参列表复制自comparator<person>.compare,即(Person, Person)
  • 它的主体调用方法是:person.comparebyage。

总结


其实,JVM 本身并不支持指向方法引用,过去不支持,现在也不支持。Java 8 对方法引用的支持只是编译器层面的支持,虚拟机执行引擎并不了解方法引用。编译器遇到方法引用的时候,会像上面那样自动推断出开发者的意图,将方法引用还原成接口实现对象,或者更形象地说,就是把方法引用设法包装成一个接口实现对象,这样虚拟机就可以无差别地执行字节码文件而不需要管什么是方法引用了。

需要注意的是,方法引用是用来简化接口实现代码的,并且凡是能够用方法引用来简化的接口,都有这样的特征:有且只有一个待实现的方法。这种接口在 Java 中有个专门的名称: 函数式接口。当试图用方法引用替代一个非函数式接口时,会有这样的错误提示: xxx is not a functional interface。