繁体   English   中英

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

[英]:: (double colon) operator in 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

通常,人们会使用Math.max(int, int)调用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 编译器“检测”到您想要实现一个方法,该方法接受两个int并返回一个int 这相当于接口IntBinaryOperator唯一方法的形IntBinaryOperator (你要调用的方法reduce的参数)。 所以编译器会为你做剩下的事情——它只是假设你想要实现IntBinaryOperator

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

reduce(Math::max);

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

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

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

简短说明
下面是一个引用静态方法的例子:

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上面是一个功能性的接口 要完全理解:: ,理解函数式接口也很重要。 简单地说, 函数式接口是只有一个抽象方法的接口。

功能接口的示例包括RunnableCallableActionListener

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需要一个参数并返回一个double 供应商中get方法返回一个值但不带参数。 因此,这会导致错误。

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

更多示例: Consumer 中accept方法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;

方法引用可以有不同的风格,但从根本上说,它们都意味着相同的事情,可以简单地可视化为 lambdas:

  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

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

可以在此处找到 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);
   }
}

似乎有点晚了,但这是我的两分钱。 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")
                );
    }

现在我们知道Comparator是一个函数式接口。 函数式接口是只有一个抽象方法的接口(尽管它可能包含一个或多个默认或静态方法)。 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                       |
+------------------------------------------------------------+--------------------------------------+

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

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

ClassName :: methodName 

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

objRef :: methodName

ClassName :: methodName

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

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

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

这是 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)。

:: 运算符是在 java 8 中引入的用于方法引用。 方法引用是只执行一个方法的 lambda 表达式的简写语法。 以下是方法引用的一般语法:

Object :: methodName

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

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

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

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

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

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

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


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

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

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

内联 lambda 表达式类似于已定义的函数式接口(该接口具有不多也不少于 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) 
                       {
                        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);

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

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

Purchase::calculatePrice

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

我发现这个来源非常有趣。

事实上,是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);

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

由于这里的许多答案都很好地解释了::行为,另外我想澄清一下,如果::运算符用于实例变量,则它不需要与引用的功能接口具有完全相同的签名 假设我们需要一个类型为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)" 所以编译器会寻找一个静态引用,如果它不是相同的类型。 好吧,多态呢? 如果我们删除 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;

*我只是在学习,所以我已经通过尝试了解了,如果我错了,请随时纠正我

在java-8 Streams Reducer 中,简单的工作是一个函数,它以两个值作为输入并在一些计算后返回结果。 这个结果被送入下一次迭代。

在 Math:max 函数的情况下,方法不断返回传递的两个值的最大值,最后你手头有最大的数字。

在较旧的 Java 版本中,您可以使用:

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

双冒号 (::) 运算符,在 Java 中也称为方法引用运算符,用于通过直接在类的帮助下引用方法来调用方法。 它们的行为与 lambda 表达式完全相同。 它与 lambda 表达式的唯一区别是它使用按名称直接引用方法,而不是为方法提供委托。

句法:

<Class name>::<method name>

此符号可用于代替 Lamda 表达式

程序:

// Java code to print the elements of Stream
// without using double colon operator
  
import java.util.stream.*;
  
class MyClass {
    public static void main(String[] args)
    {
  
        // Get the stream
        Stream<String> stream
            = Stream.of("Testing","Program");
  
        // Print the stream
        stream.forEach(s -> System.out.println(s));
    }
}

输出:

Testing
Program

stream.forEach(s -> System.out.println(s)); 可以替换为

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

编程时也经常使用第一种方法。

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

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

关于::方法引用的作用,前面的答案非常完整。 总而言之,它提供了一种无需执行即可引用方法(或构造函数)的方法,并且在求值时,它会创建提供目标类型上下文的函数式接口的实例。

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


不使用::

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

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

类名::方法名

前任:-

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

通过使用双冒号::

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM