ChatGPT解决这个技术问题 Extra ChatGPT

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

我正在探索 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

让编译器根据您提供的函数自动生成接口实现是语法糖(使整个 lambda 更容易与现有代码库一起使用)。
java.dzone.com/articles/java-lambda-expressions-vs 可能会有所帮助,但没有深入探讨该主题
@Neet 它不完全是“语法糖”,除非你能说出什么。即“x 是 y 的语法糖”。
@Ingo 每次我使用它时它都会创建一个新的 lambda 对象。 TestingLambda$$Lambda$2/8460669TestingLambda$$Lambda$3/11043253 是在两次调用中创建的。
Lambda 和方法引用不是“普通的旧匿名内部类”。请参阅programmers.stackexchange.com/a/181743/59134。是的,如有必要,新的类和实例会在必要时即时创建,但仅限于必要时。

J
Jared Burrows

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

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 编译器“检测到”您想要实现一个接受两个 int 并返回一个 int 的方法。这相当于接口 IntBinaryOperator 的唯一方法的形式参数(您要调用的方法 reduce 的参数)。所以编译器会为您完成其余的工作 - 它只是假设您想要实现 IntBinaryOperator

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

reduce(Math::max);

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

(另见Translation of Lambda Expressions


P
Peter Mortensen

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

简短说明:

下面是引用静态方法的示例:

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 是一个功能界面。要完全理解 ::,理解函数式接口也很重要。很明显,functional interface 是一个只有一个抽象方法的接口。

功能接口的示例包括 RunnableCallableActionListener

上面的 Function 是一个功能接口,只有一个方法:apply。它接受一个参数并产生一个结果。

:: 很棒的原因是 that

方法引用是与 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:

静态方法 (ClassName::methName) 特定对象的实例方法 (instanceRef::methName) 特定对象的超级方法 (super::methName) 特定类型的任意对象的实例方法 (ClassName:: methName) 类构造函数引用 (ClassName::new) 数组构造函数引用 (TypeName[]::new)

如需进一步参考,请参阅 State of the Lambda


谢谢你的解释。总之:'::'用于提取满足FunctionalInterface(lambda)的方法:ClassX::staticMethodX,或instanceX::instanceMethodX"
嗨@Jatin,这不是因为方法不是First-class citizen而违反OOP范式吗?
A
ABC123

是的,这是真的。 :: 运算符用于方法引用。因此,可以通过使用它从类中提取静态方法或从对象中提取方法。相同的运算符甚至可以用于构造函数。这里提到的所有案例都在下面的代码示例中进行了举例说明。

可以在 here 中找到 Oracle 的官方文档。

您可以在 this 文章中更好地了解 JDK 8 的变化。在 Method/Constructor references 部分还提供了一个代码示例:

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/…
@RichardTingle method(Math::max); 是调用,方法的定义类似于 public static void method(IntBinaryOperator op){System.out.println(op.applyAsInt(1, 2));}。这就是它的使用方式。
对于熟悉 C# 的人来说,它类似于 DelegateType d = new DelegateType(MethodName);
P
Peter Mortensen

lambda expression 用于创建匿名方法。它除了调用现有方法之外什么也不做,但直接通过方法名称来引用该方法会更清楚。 method reference 使我们能够使用方法引用运算符 :: 来做到这一点。

考虑以下简单的类,其中每个员工都有一个姓名和等级。

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;
    }
}

假设我们有一个通过某种方法返回的员工列表,我们想按员工的等级对员工进行排序。我们知道我们可以将 anonymous class 用作:

    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")
                );
    }

现在我们知道 Comparator 是一个函数式接口functional interface 是只有一个抽象方法的那个(尽管它可能包含一个或多个默认或静态方法)。 Lambda 表达式提供了 @FunctionalInterface 的实现,因此函数式接口只能有一个抽象方法。我们可以将 lambda 表达式用作:

employeeList.sort((e1,e2) -> e1.getGrade().compareTo(e2.getGrade())); // Lambda expression

看起来一切都很好,但是如果类 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); // 方法参考

根据 the documentation,有四种方法引用:

+----+-------------------------------------------------------+--------------------------------------+
|    | 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                       |
+------------------------------------------------------------+--------------------------------------+

我不明白需要两个参数的 compareByGrade 是如何简单地称为 Employee::compareByGrade 的。它如何知道要比较哪两个员工?我猜,因为它是在 sort() 中调用的,编译器会自动迭代数组的所有成员?您将如何采取相反的方式并指定要比较的两个特定对象?
@NathanielHoyt Javas 标准 List 接口具有 sort 方法,该方法采用 Comparator 来对 List 进行排序。这就是此代码中使用的内容。这是在 Java 8 中添加的。
P
Peter Mortensen

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

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

ClassName :: methodName 

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

objRef :: methodName

ClassName :: methodName

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

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

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


链接(实际上)已损坏:“此域名出售:2,695 美元”
P
Peter Mortensen

这是 Java 8 中的方法参考。Oracle 文档是 here

如文档中所述...

方法引用 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)。


但“compareByAge”方法不是静态的。
@abbas 也不是 compareByName。因此,您可以使用对象通过引用运算符访问这些非静态方法。如果它们是静态的,则可以使用 ComparisionProvider::someStaticMethod 之类的类名
P
Peter Mortensen

:: 运算符是在 Java 8 中引入的,用于方法引用。方法引用是仅执行一个方法的 lambda 表达式的简写语法。这是方法引用的一般语法:

Object :: methodName

我们知道我们可以使用 lambda expressions 而不是使用匿名类。但有时,lambda 表达式实际上只是对某个方法的调用,例如:

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

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

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

伟大而简单的提示!转到消费者的接受呼叫:c.accept(s);
谢谢。简单的解释/例子,告诉我我想知道和需要知道什么。所以:: 基本上是一个更短的 lambda 表达式。所以 object::nonstaticmethod()-> object.nonstaticmethod()event-> object.nonstaticmethod()。对于 class.method(),只需 class::method
P
Peter Mortensen

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

答案很简单:**:: 被称为方法引用Method References 如果您向下滚动到表格,您可以找到所有信息。

现在,让我们简要了解一下什么是方法引用:

A::b 在某种程度上替代了以下内联 lambda 表达式:(parameters ...) -> Ab(parameter ...)

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

内联 lambda 表达式类似于已定义的函数式接口(该接口具有不多也不少于一个方法)。

让我们简要了解一下我的意思:

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)
                        {
                            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);

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


P
Peter Mortensen

我发现 this source 非常有趣。

实际上,正是 lambda 变成了双冒号。双冒号更具可读性。

我们遵循以下步骤:

步骤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());

第 3 步

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

似乎 Person::getAge() 应该是 Person::getAge
P
Peter Mortensen

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

Purchase::calculatePrice

它也可以看作是编写 lambda 表达式的一种简写形式,因为方法引用被转换为 lambda 表达式。


我可以进行嵌套方法引用吗?例如 groupingBy( Order::customer::name )
您不能以这种方式进行嵌套方法引用
K
Kamil Tomasz Jarmusik

在较旧的 Java 版本中,您可以使用:而不是“::”或 lambda:

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();
}

回复“lambd”:你的意思是“lambda”吗?
P
Peter Mortensen

在 Java 8 中,Streams Reducer 简单地作为一个函数工作,它接受两个值作为输入,并在一些计算后返回结果。这个结果被输入到下一次迭代中。

在 Math:max 函数的情况下,该方法会不断返回传递的两个值中的最大值,最后你会得到最大的数字。


“简单的 Streams Reducer”是什么意思?你能详细说明吗?
P
Peter Mortensen

由于这里的许多答案很好地解释了 :: 行为,另外我想澄清的是,如果将 :: 运算符用于实例变量,则它不需要与引用 功能接口 具有完全相同的签名.假设我们需要一个类型为 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;

我的 Eclipse 告诉我“无法从类型 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 吗? IE:

public class TestUtil {

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

并调用:

BinaryOperator<TestObject> binary = TestUtil::testInstance;

它不会编译,编译器会说:“TestUtil 类型没有定义 testInstance(TestObject, TestObject)”。

因此,如果它不是同一类型,编译器将查找静态引用。好的,多态性呢?如果我们删除 final 修饰符并添加我们的 SubTestObject 类:

public class SubTestObject extends TestObject {

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

}

并调用:

BinaryOperator<TestObject> binary = SubTestObject::testInstance;

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

public class TestObject {

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

}

BinaryOperator<TestObject> binary = TestObject::testInstance;

P
Peter Mortensen

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 ^_^

P
Peter Mortensen

关于 :: 方法参考的作用,前面的答案非常完整。总而言之,它提供了一种在不执行的情况下引用方法(或构造函数)的方法,并且在评估时,它会创建提供目标类型上下文的功能接口的实例。

以下是使用 :: 方法引用在 ArrayList withwithout 中查找具有最大值的对象的两个示例。解释在下面的评论中。

没有使用 ::

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);
    }
}

P
Peter Mortensen

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

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


javaap -c;) 不可能是正确的。你能详细说明吗?
P
Peter Mortensen

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

类名::方法名

例子:

stream.forEach(元素-> System.out.println(元素))

通过使用 双冒号 ::

stream.forEach(System.out::println(element))