Java 8中的::(双冒号)运算符


955

我正在探索Java 8源代码,发现代码的这一特殊部分非常令人惊讶:

//defined in IntPipeline.java
@Override
public final OptionalInt reduce(IntBinaryOperator op) {
    return evaluate(ReduceOps.makeInt(op));
}

@Override
public final OptionalInt max() {
    return reduce(Math::max); //this is the gotcha line
}

//defined in Math.java
public static int max(int a, int b) {
    return (a >= b) ? a : b;
}

Math::max类似方法指针的东西吗?普通static方法如何转换为IntBinaryOperator


60
使编译器根据您提供的功能自动生成接口实现是一种语法上的糖(使整个lambda变得更易于在现有代码库中使用)。
Neet

4
java.dzone.com/articles/java-lambda-expressions-vs可能有所帮助,但并未深入探讨该主题
Pontus Backlund 2013年

8
@Neet并非完全是“语法糖”,除非您可以说些什么。即“ x是y的语法糖”。
Ingo 2013年

6
@Ingo每次使用它都会创建一个新的lambda对象。TestingLambda$$Lambda$2/8460669并且TestingLambda$$Lambda$3/11043253是在两个调用上创建的。
Narendra Pathai

13
Lambda和方法引用不是“普通的旧匿名内部类”。请参阅programmers.stackexchange.com/a/181743/59134。是的,如果有必要,则在必要时(但仅在必要时)动态创建新的类和实例。
斯图尔特(Stuart Marks)

Answers:


1022

通常,可以reduce使用Math.max(int, int)以下方法调用该方法:

reduce(new IntBinaryOperator() {
    int applyAsInt(int left, int right) {
        return Math.max(left, right);
    }
});

仅调用就需要很多语法Math.max。那就是lambda表达式起作用的地方。从Java 8开始,允许以更短的方式执行相同的操作:

reduce((int left, int right) -> Math.max(left, right));

这是如何运作的?Java编译器“检测”您要实现一个接受两个ints并返回一个的方法int。这等效于接口的唯一方法的形式参数IntBinaryOperatorreduce要调用的方法的参数)。因此,编译器会为您完成其余工作-只是假设您要实现IntBinaryOperator

但是,由于Math.max(int, int)其本身满足的形式要求IntBinaryOperator,因此可以直接使用。由于Java 7没有任何语法允许将方法本身作为参数传递(您只能传递方法结果,而不能传递方法引用),因此::Java 8 在语法中引入了引用方法:

reduce(Math::max);

注意,这将由编译器解释,而不是在运行时由JVM解释!尽管它为所有三个代码段生成了不同的字节码,但它们在语义上是相等的,因此,可以认为后两个是该代码段的简短版本(可能效率更高)。IntBinaryOperator

(另请参见Lambda表达式的翻译


489

::称为方法参考。它基本上是对单个方法的引用。即,它通过名称引用现有方法。

简短说明
以下是对静态方法的引用示例:

class Hey {
     public static double square(double num){
        return Math.pow(num, 2);
    }
}

Function<Double, Double> square = Hey::square;
double ans = square.apply(23d);

square可以像对象引用一样传递,并在需要时触发。实际上,它就像引用对象的“正常”方法一样容易使用static。例如:

class Hey {
    public double square(double num) {
        return Math.pow(num, 2);
    }
}

Hey hey = new Hey();
Function<Double, Double> square = hey::square;
double ans = square.apply(23d);

Function以上是一个功能接口。要完全理解::,理解功能接口也很重要。显然,功能接口是仅具有一种抽象方法的接口。

功能接口的例子包括RunnableCallable,和ActionListener

Function以上是仅一种方法的功能接口:apply。它接受一个参数并产生结果。


为什么原因::s为真棒是

方法引用是与lambda表达式(...)相同的表达式,但是它们不是提供方法主体,而是通过名称引用现有方法。

例如,而不是编写lambda正文

Function<Double, Double> square = (Double x) -> x * x;

你可以做

Function<Double, Double> square = Hey::square;

在运行时,这两种square方法的行为完全相同。字节码可以相同也可以不相同(不过,对于上述情况,会生成相同的字节码;请编译以上内容并使用进行检查javap -c)。

要满足的唯一主要标准是:您提供的方法应该与用作对象引用的功能接口的方法具有相似的签名

以下是非法的:

Supplier<Boolean> p = Hey::square; // illegal

square需要一个参数并返回doubleSupplier中get方法返回一个值,但不接受参数。因此,这导致错误。

方法参考是指功能接口的方法。(如前所述,每个功能接口只能有一种方法)。

还有更多示例:Consumer中accept方法接受输入,但不返回任何内容。

Consumer<Integer> b1 = System::exit;   // void exit(int status)
Consumer<String[]> b2 = Arrays::sort;  // void sort(Object[] a)
Consumer<String> b3 = MyProgram::main; // void main(String... args)

class Hey {
    public double getRandom() {
        return Math.random();
    }
}

Callable<Double> call = hey::getRandom;
Supplier<Double> call2 = hey::getRandom;
DoubleSupplier sup = hey::getRandom;
// Supplier is functional interface that takes no argument and gives a result

上面,getRandom不带参数并返回double。因此,可以使用满足以下条件的任何功能接口:不带参数并返回double

另一个例子:

Set<String> set = new HashSet<>();
set.addAll(Arrays.asList("leo","bale","hanks"));
Predicate<String> pred = set::contains;
boolean exists = pred.test("leo");

如果是参数化类型

class Param<T> {
    T elem;
    public T get() {
        return elem;
    }

    public void set(T elem) {
        this.elem = elem;
    }

    public static <E> E returnSame(E elem) {
        return elem;
    }
}

Supplier<Param<Integer>> obj = Param<Integer>::new;
Param<Integer> param = obj.get();
Consumer<Integer> c = param::set;
Supplier<Integer> s = param::get;

Function<String, String> func = Param::<String>returnSame;

方法引用可以具有不同的样式,但从根本上讲,它们都表示同一件事,并且可以简单地可视化为lambda:

  1. 静态方法(ClassName::methName
  2. 特定对象的实例方法(instanceRef::methName
  3. 特定对象的超级方法(super::methName
  4. 特定类型(ClassName::methName)的任意对象的实例方法
  5. 类构造函数参考(ClassName::new
  6. 数组构造函数参考(TypeName[]::new

有关更多参考,请参见http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html


6
谢谢你的解释。总结:“ ::”用于提取满足FunctionalInterface(lambda)的方法:ClassX :: staticMethodX或instanceX :: instanceMethodX“
jessarah,2016年

55

是的,那是真的。的::运算符用于方法参照。因此,可以通过使用静态方法或对象的方法来提取静态方法。即使对于构造函数,也可以使用相同的运算符。此处提到的所有情况在下面的代码示例中得到了示例。

Oracle的官方文档可以在这里找到。

您可以在JDK 8的变化,更好地观察这个文章。在“ 方法/构造函数引用”部分中,还提供了代码示例:

interface ConstructorReference {
    T constructor();
}

interface  MethodReference {
   void anotherMethod(String input);
}

public class ConstructorClass {
    String value;

   public ConstructorClass() {
       value = "default";
   }

   public static void method(String input) {
      System.out.println(input);
   }

   public void nextMethod(String input) {
       // operations
   }

   public static void main(String... args) {
       // constructor reference
       ConstructorReference reference = ConstructorClass::new;
       ConstructorClass cc = reference.constructor();

       // static method reference
       MethodReference mr = cc::method;

       // object method reference
       MethodReference mr2 = cc::nextMethod;

       System.out.println(cc.value);
   }
}

一个很好的解释是在这里找到的一个:doanduyhai.wordpress.com/2012/07/14/…–
Olimpiu POP,

1
@RichardTingle method(Math::max);正在调用,方法的定义将类似于public static void method(IntBinaryOperator op){System.out.println(op.applyAsInt(1, 2));}。这就是它的用法。
Narendra Pathai 2013年

2
对于熟悉C#的人,它与DelegateType类似。
Adrian Zanescu 2013年

27

似乎来晚了,但这是我的两分钱。甲lambda表达式用于创建匿名方法。它除了调用现有方法外什么也不做,但是更清楚地直接通过名称来引用该方法。和方法的参考,使我们能够做到这一点的使用方法,参考操作::

考虑下面的简单类,其中每个雇员都有姓名和职级。

public class Employee {
    private String name;
    private String grade;

    public Employee(String name, String grade) {
        this.name = name;
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGrade() {
        return grade;
    }

    public void setGrade(String grade) {
        this.grade = grade;
    }
}

假设我们有通过某种方法返回的员工列表,并且我们想按员工的等级对其进行排序。我们知道我们可以将匿名类用作:

    List<Employee> employeeList = getDummyEmployees();

    // Using anonymous class
    employeeList.sort(new Comparator<Employee>() {
           @Override
           public int compare(Employee e1, Employee e2) {
               return e1.getGrade().compareTo(e2.getGrade());
           }
    });

其中getDummyEmployee()是如下方法:

private static List<Employee> getDummyEmployees() {
        return Arrays.asList(new Employee("Carrie", "C"),
                new Employee("Fanishwar", "F"),
                new Employee("Brian", "B"),
                new Employee("Donald", "D"),
                new Employee("Adam", "A"),
                new Employee("Evan", "E")
                );
    }

现在我们知道比较器是一个功能接口。功能接口是仅具有一种抽象方法的功能接口(尽管它可能包含一个或多个默认或静态方法)。Lambda表达式提供的实现,@FunctionalInterface因此功能接口只能具有一种抽象方法。我们可以将lambda表达式用作:

employeeList.sort((e1,e2) -> e1.getGrade().compareTo(e2.getGrade())); // lambda exp

看起来一切都很好,但是如果该类Employee还提供了类似的方法,该怎么办:

public class Employee {
    private String name;
    private String grade;
    // getter and setter
    public static int compareByGrade(Employee e1, Employee e2) {
        return e1.grade.compareTo(e2.grade);
    }
}

在这种情况下,使用方法名称本身将更加清晰。因此,我们可以通过使用方法引用直接引用方法:

employeeList.sort(Employee::compareByGrade); // method reference

根据文档,有四种方法参考:

+----+-------------------------------------------------------+--------------------------------------+
|    | Kind                                                  | Example                              |
+----+-------------------------------------------------------+--------------------------------------+
| 1  | Reference to a static method                          | ContainingClass::staticMethodName    |
+----+-------------------------------------------------------+--------------------------------------+
| 2  |Reference to an instance method of a particular object | containingObject::instanceMethodName | 
+----+-------------------------------------------------------+--------------------------------------+
| 3  | Reference to an instance method of an arbitrary object| ContainingType::methodName           |
|    | of a particular type                                  |                                      |  
+----+-------------------------------------------------------+--------------------------------------+
| 4  |Reference to a constructor                             | ClassName::new                       |
+------------------------------------------------------------+--------------------------------------+

25

::是Java 8中包含的新运算符,用于引用现有类的方法。您可以引用类的静态方法和非静态方法。

对于引用静态方法,语法为:

ClassName :: methodName 

对于引用非静态方法,语法为

objRef :: methodName

ClassName :: methodName

引用方法的唯一前提条件是该方法存在于功能接口中,该接口必须与方法引用兼容。

方法引用在评估后会创建功能接口的实例。

发现于:http : //www.speakingcs.com/2014/08/method-references-in-java-8.html


22

这是Java 8中的方法参考。oracle文档在此处

如文档中所述...

方法引用Person :: compareByAge是对静态方法的引用。

以下是对特定对象的实例方法的引用示例:

class ComparisonProvider {
    public int compareByName(Person a, Person b) {
        return a.getName().compareTo(b.getName());
    }

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

ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName); 

方法引用myComparisonProvider :: compareByName调用方法compareByName,它是对象myComparisonProvider的一部分。JRE推断方法类型参数,在这种情况下为(Person,Person)。


2
但是方法“ compareByAge”不是静态的。
阿巴斯(Abbas)'16

3
@abbas也不是compareByName。因此,您可以使用对象通过引用运算符访问这些非静态方法。如果他们是静态的,你可以使用类名状ComparisionProvider :: someStaticMethod
Seshadri [R

6

在Java 8中引入了::运算符以供方法参考。方法参考是仅执行一个方法的lambda表达式的简写语法。这是方法参考的一般语法:

Object :: methodName

我们知道我们可以使用lambda表达式来代替匿名类。但是有时候,lambda表达式实际上只是对某些方法的调用,例如:

Consumer<String> c = s -> System.out.println(s);

为了使代码更清晰,您可以将lambda表达式转换为方法引用:

Consumer<String> c = System.out::println;

3

::被称为方法引用。假设我们要调用Purchase类的calculatePrice方法。然后我们可以这样写:

Purchase::calculatePrice

也可以将其视为编写lambda表达式的简短形式,因为方法引用已转换为lambda表达式。


我可以做嵌套的方法引用吗?例如groupingBy(Order :: customer :: name)

您不能以这种方式进行嵌套方法引用
Kirby

3

我发现此来源非常有趣。

实际上,正是Lambda变成了Double Colon。双冒号更具可读性。我们遵循以下步骤:

步骤1:

// We create a comparator of two persons
Comparator c = (Person p1, Person p2) -> p1.getAge().compareTo(p2.getAge());

第2步:

// We use the interference
Comparator c = (p1, p2) -> p1.getAge().compareTo(p2.getAge());

第三步:

// The magic using method reference
Comparator c = Comparator.comparing(Person::getAge);

3
似乎Person::getAge()应该是Person::getAge
Qwertiy

2

return reduce(Math::max);不等于return reduce(max());

但这意味着,像这样:

IntBinaryOperator myLambda = (a, b)->{(a >= b) ? a : b};//56 keystrokes I had to type -_-
return reduce(myLambda);

如果您这样编写,则仅可以保存47次击键

return reduce(Math::max);//Only 9 keystrokes ^_^

2

由于此处的许多答案都很好地解释了::行为,因此我想澄清一下,如果将:: 运算符用于实例变量,则运算符不需要具有与引用函数接口完全相同的签名。假设我们需要一个类型为TestObjectBinaryOperator。按照传统方式,其实现方式如下:

BinaryOperator<TestObject> binary = new BinaryOperator<TestObject>() {

        @Override
        public TestObject apply(TestObject t, TestObject u) {

            return t;
        }
    };

如您在匿名实现中所看到的,它需要两个TestObject参数,并且还返回一个TestObject对象。为了通过使用::运算符来满足此条件,我们可以从静态方法开始:

public class TestObject {


    public static final TestObject testStatic(TestObject t, TestObject t2){
        return t;
    }
}

然后致电:

BinaryOperator<TestObject> binary = TestObject::testStatic;

好的,编译良好。如果我们需要实例方法呢?让我们用实例方法更新TestObject:

public class TestObject {

    public final TestObject testInstance(TestObject t, TestObject t2){
        return t;
    }

    public static final TestObject testStatic(TestObject t, TestObject t2){
        return t;
    }
}

现在我们可以访问实例如下:

TestObject testObject = new TestObject();
BinaryOperator<TestObject> binary = testObject::testInstance;

这段代码可以很好地编译,但是不能编译以下代码:

BinaryOperator<TestObject> binary = TestObject::testInstance;

月食告诉我 “无法从类型TestObject静态引用非静态方法testInstance(TestObject,TestObject)...”

足够公平的是它的一个实例方法,但是如果我们testInstance如下重载:

public class TestObject {

    public final TestObject testInstance(TestObject t){
        return t;
    }

    public final TestObject testInstance(TestObject t, TestObject t2){
        return t;
    }

    public static final TestObject testStatic(TestObject t, TestObject t2){
        return t;
    }
}

并致电:

BinaryOperator<TestObject> binary = TestObject::testInstance;

该代码将编译正常。因为它将testInstance使用单参数而不是双参数来调用。好吧,我们的两个参数发生了什么?让我们打印输出并查看:

public class TestObject {

    public TestObject() {
        System.out.println(this.hashCode());
    }

    public final TestObject testInstance(TestObject t){
        System.out.println("Test instance called. this.hashCode:" 
    + this.hashCode());
        System.out.println("Given parameter hashCode:" + t.hashCode());
        return t;
    }

    public final TestObject testInstance(TestObject t, TestObject t2){
        return t;
    }

    public static final TestObject testStatic(TestObject t, TestObject t2){
        return t;
    }
}

将输出:

 1418481495  
 303563356  
 Test instance called. this.hashCode:1418481495
 Given parameter hashCode:303563356

好的,JVM足够聪明,可以调用param1.testInstance(param2)。我们可以使用testInstance其他资源但不能使用TestObject 吗,即:

public class TestUtil {

    public final TestObject testInstance(TestObject t){
        return t;
    }
}

并致电:

BinaryOperator<TestObject> binary = TestUtil::testInstance;

它不会编译,编译器会告诉您:“类型TestUtil没有定义testInstance(TestObject,TestObject)”。因此,如果编译器不是同一类型,则它将查找静态引用。好吧,多态性呢?如果删除最终修饰符并添加SubTestObject类:

public class SubTestObject extends TestObject {

    public final TestObject testInstance(TestObject t){
        return t;
    }

}

并致电:

BinaryOperator<TestObject> binary = SubTestObject::testInstance;

它也不会编译,编译器仍会寻找静态引用。但是下面的代码可以编译良好,因为它通过了-测试:

public class TestObject {

    public SubTestObject testInstance(Object t){
        return (SubTestObject) t;
    }

}

BinaryOperator<TestObject> binary = TestObject::testInstance;

*我只是在学习,所以我通过尝试来弄清楚了,如果我错了,请随时纠正我


2

在java-8 Streams Reducer中,简单的工作是一个函数,该函数将两个值作为输入并在进行一些计算后返回结果。该结果将在下一次迭代中提供。

在使用Math:max函数的情况下,方法将始终返回所传递的两个值的最大值,最后您手中的数字最大。


1

在运行时它们的行为完全相同。字节码可能/不相同(对于上述Incase,它生成相同的字节码(在上面编译并检查javaap -c;))

在运行时,它们的行为完全相同。method(math :: max);生成相同的数学运算(在上面编译并检查javap -c;))


1

在较早的Java版本中,您可以使用以下命令来代替“ ::”或lambd。

public interface Action {
    void execute();
}

public class ActionImpl implements Action {

    @Override
    public void execute() {
        System.out.println("execute with ActionImpl");
    }

}

public static void main(String[] args) {
    Action action = new Action() {
        @Override
        public void execute() {
            System.out.println("execute with anonymous class");
        }
    };
    action.execute();

    //or

    Action actionImpl = new ActionImpl();
    actionImpl.execute();
}

或传递给方法:

public static void doSomething(Action action) {
    action.execute();
}

1

因此,我在这里看到了很多答案,坦率地说过于复杂,这是一种轻描淡写的说法。

答案很简单:::被称为方法参考 https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

因此,我不会复制粘贴该链接,如果您向下滚动到表格,则可以找到所有信息。


现在,让我们简短地看一下什么是方法引用:

A :: B在 某种程度上替代了以下内联lambda表达式(params ...)-> AB(params ...)

要将其与您的问题相关联,有必要了解一个Java lambda表达式。这不难。

内联lambda表达式类似于已定义的 功能接口(该接口的方法不超过1个,且不少于1个)。让我们简短地看一下我的意思:

InterfaceX f = (x) -> x*x; 

InterfaceX必须是功能接口。任何功能接口,对于该编译器而言,InterfaceX唯一重要的是定义格式:

InterfaceX可以是以下任意一种:

interface InterfaceX
{
    public Integer callMe(Integer x);
}

或这个

interface InterfaceX
{
    public Double callMe(Integer x);
}

或更通用的:

interface InterfaceX<T,U>
{
    public T callMe(U x);
}

让我们来看一下第一个展示的案例和我们之前定义的内联lambda表达式。

在Java 8之前,您可以通过以下类似方式进行定义:

 InterfaceX o = new InterfaceX(){
                     public int callMe (int x, int y) 
                       {
                        return x*x;
                       } };

从功能上讲,这是同一回事。区别更多在于编译器如何理解这一点。

现在我们来看一下内联lambda表达式,让我们回到“方法引用(::)”。假设您有一个这样的课程:

class Q {
        public static int anyFunction(int x)
             {
                 return x+5;
             } 
        }

由于方法anyFunctions具有与InterfaceX callMe相同的类型,因此我们可以使用“方法引用”来等效这两种方法。

我们可以这样写:

InterfaceX o =  Q::anyFunction; 

这等效于:

InterfaceX o = (x) -> Q.anyFunction(x);

方法引用的一个很酷的优点是,首先,在将它们分配给变量之前,它们是无类型的。因此,您可以将它们作为参数传递给任何等效的外观(具有相同的定义类型)功能接口。这正是您的情况


1

先前的答案关于什么 ::方法引用的作用。综上所述,它提供了一种无需执行即可引用方法(或构造函数)的方法,并且在进行评估时,它会创建提供目标类型上下文的功能接口的实例。

下面是两个示例,ArrayList它们使用::方法引用在WITH和WITHOUT中查找具有最大值的对象。解释在下面的注释中。


不使用 ::

import java.util.*;

class MyClass {
    private int val;
    MyClass (int v) { val = v; }
    int getVal() { return val; }
}

class ByVal implements Comparator<MyClass> {
    // no need to create this class when using method reference
    public int compare(MyClass source, MyClass ref) {
        return source.getVal() - ref.getVal();
    }
}

public class FindMaxInCol {
    public static void main(String args[]) {
        ArrayList<MyClass> myClassList = new ArrayList<MyClass>();
        myClassList.add(new MyClass(1));
        myClassList.add(new MyClass(0));
        myClassList.add(new MyClass(3));
        myClassList.add(new MyClass(6));

        MyClass maxValObj = Collections.max(myClassList, new ByVal());
    }
}

与使用 ::

import java.util.*;

class MyClass {
    private int val;
    MyClass (int v) { val = v; }
    int getVal() { return val; }
}

public class FindMaxInCol {
    static int compareMyClass(MyClass source, MyClass ref) {
        // This static method is compatible with the compare() method defined by Comparator. 
        // So there's no need to explicitly implement and create an instance of Comparator like the first example.
        return source.getVal() - ref.getVal();
    }

    public static void main(String args[]) {
        ArrayList<MyClass> myClassList = new ArrayList<MyClass>();
        myClassList.add(new MyClass(1));
        myClassList.add(new MyClass(0));
        myClassList.add(new MyClass(3));
        myClassList.add(new MyClass(6));

        MyClass maxValObj = Collections.max(myClassList, FindMaxInCol::compareMyClass);
    }
}

-1

::Java 8中引入了双冒号即运算符作为方法参考。方法引用是lambda表达式的一种形式,用于通过名称来引用现有方法。

classname :: methodName

例如:-

  • stream.forEach(element -> System.out.println(element))

通过使用双冒号 ::

  • stream.forEach(System.out::println(element))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.