java 无参数的toUpperCase()方法如何实现Function.apply()?[duplicate]

fhg3lkii  于 2023-01-01  发布在  Java
关注(0)|答案(2)|浏览(120)
    • 此问题在此处已有答案**:

How does method reference casting work?(3个答案)
3天前关闭。
此帖子已于3天前编辑并提交审核,未能重新打开帖子:
原始关闭原因未解决
我正在学习Java 8中的Lambda、Streams和方法引用。

Optional<String> s = Optional.of("test");
System.out.println(s.map(String::toUpperCase).get());

我不明白怎么可能使用String::toUpperCase作为map()方法的输入。
以下是方法实现:

public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

所以它需要一个函数接口,并且有这个apply()方法:R apply(T t);这个方法有一个输入参数。
toUpperCase()方法没有任何参数:

public String toUpperCase() {
    return toUpperCase(Locale.getDefault());
}

如果abstract方法apply(T t)有 * 一个 * 参数,那么实现的方法应该有 * 一个 * 相同类型的参数。无参数方法toUpperCase()如何从函数接口实现apply(T t)方法?
我试着重现同样的情况:
我创建了一个函数接口:

public interface Interf {
    String m1(String value);
}

然后我创建一个类,其中包含m1()的方法引用:

public class Impl {
    public String value;

    public String toUpp() {
        return value.toUpperCase();
    }
}

下面是一个测试类:

public class Test {
    public static void main(String[] args) {
        Interf i = String::toUpperCase;
        System.out.println(i.m1("hey"));

        Interf i1 = Impl::toUpp;
        System.out.println(i.m1("hello"));
    }
}

此语句没有任何问题:Interf i = String::toUpperCase;,但此行存在编译错误:Interf i1 = Impl::toUpp;。它说:
不能从静态上下文引用非静态方法
但是toUpperCase()也是一个非静态方法,即使我把toUpp()设为静态,它仍然不起作用,只有当我添加一个String参数作为toUpp()的输入参数时,它才起作用,但是为什么它对String::toUpperCase起作用呢?

7cwmlq89

7cwmlq891#

TL; DR

根据方法引用实现的Functional接口所施加的约定,方法引用预期使用的参数必须与方法引用中使用的方法参数相同。
这个答案是从这个常见的误解到理解方法引用的所有语法风格的一个旅程。
让我们从Method引用的定义开始,采取一些小步骤来消除误解。

什么是方法引用

以下是根据Java语言规范§ 15.13的方法引用的定义。

    • 方法引用表达式用于引用一个方法的调用,而不是实际执行*****************************************************************************************************************************************************************************************************************************

着重号后加
因此,Method引用是一种引用方法调用而不调用方法的方式,或者换句话说,它是一种通过委托现有功能来描述特定行为的方式。
让我们绕个小圈子,看看Method引用的兄弟,一个Lambda表达式。
Lambda表达式也是一种描述行为的方式(不执行它),并且lambdas和Method引用都应该符合Functional接口。
考虑一下,我们有一个域类Foo和实用程序类FooUtils

public class FooUtils {

    public static Foo doSomethingWithFoo(Foo foo) {
        // do something
        return new Foo();
    }
}

我们需要定义一个UnaryOperator<Foo>类型的函数,让我们从写一个lambda表达式开始:

UnaryOperator<Foo> fooChanger = foo -> FooUtils.doSomethingWithFoo(foo);

Lambda接收一个Foo的示例作为参数,并将其提供给现有的实用方法。非常简单,对吧?在lambda的主体中没有发生任何特殊的事情,并且由于已经将类型定义为UnaryOperator<Foo>,lambda应该期望Foo。所有事情都是绝对可预测的,不是吗?现在的问题是:我们能换个主题吗?
当然,我们可以!

UnaryOperator<Foo> fooChanger = FooUtils::doSomethingWithFoo;

这就是Method引用的作用所在,它提供了一个简短的语法:

    • 1.**删除lambda的参数(它们仍然存在,我们只是不显示它们,因为我们知道它们是什么)。
    • 2.**删除方法名后面的括号。同样,方法声明是已知的(假设没有二义性),我们没有对参数执行任何先前的转换,也没有使用任何额外的参数,除了那些应该根据Functional接口契约来的参数。只有在这种情况下,一切都是可预测的,并且可以是方法引用。

∮ ∮ ∮ ∮ ∮一米六一分

  • 你可以把方法引用看作是缩短的lambda
  • 方法引用的参数与等价的lambda接收的参数相同,因为它们是同一接口的实现。2这些参数仍然隐式地存在,只是为了简洁而被删除。3更重要的是,方法引用使用的参数不应与它引用的方法所需的参数混淆。换句话说,方法引用的输入的第一个参数(并且它们符合接口定义的契约),而后者与引用内部发生的事情有关,与前者没有联系。

更多示例

让我们再看几个例子,假设我们有一个简单的对象来表示一个硬币,它有一个属性isHeads来描述硬币的哪一面(即heads or tails)。

public static class Coin {
    public static final Random RAND = new Random();

    private final boolean isHeads;

    public Coin() {
        this.isHeads = RAND.nextBoolean();
    }

    private Coin(boolean isHeads) {
        this.isHeads = isHeads;
    }

    public Coin reverse() {
        return new Coin(!isHeads);
    }

    public boolean isHeads() {
        return isHeads;
    }
}

让我们生成一个硬币,为此我们可以使用Supplier的实现,它非常慷慨,供应商不接收参数,它产生一个值,让我们定义一个lambda和一个引用

Supplier<Coin> coinProducer = () -> new Coin(); // no argument required according to the contract of Supplier
Supplier<Coin> coinProducer1 = Coin::new;       // Supplier expressed as a reference to no-args constructor

两者都不接收任何参数(根据供应商的合同),两者都引用无参数构造函数。
现在让我们考虑一下通过lambda和方法引用来确定硬币是否显示正面的 predicate :

Predicate<Coin> isHeads = coin -> coin.isHeads();
Predicate<Coin> isHeads1 = Coin::isHeads;

同样,lambda和方法引用都符合 predicate 的契约,并且都接收Coin的示例作为参数(否则不可能,方法引用的简单语法并没有显示这一点)。
到目前为止,一切顺利吗?让我们进一步尝试另一种方法来获得Coin,让我们定义一个Function

Function<Boolean, Coin> booleanToCoin = value -> new Coin(value);
Function<Boolean, Coin> booleanToCoin1 = Coin::new;

现在lambda和引用都使用boolean值并使用参数化构造函数。没有注意到描述Supplier<Coin>Function<Boolean, Coin>的方法引用看起来相同。

提醒:Lambda表达式和方法引用本身都没有类型。它们是所谓的多表达式,这意味着编译器应根据它们出现的上下文来推断它们的类型。lambda和引用都应符合函数接口,并且它们实现的接口指定它们是谁以及它们正在做什么。

在前面描述的所有例子中,被方法引用消耗的参数看起来和被引用的方法所期望的参数是一样的,但是它们必须是相同的,现在是时候检查一些例子了,在这些例子中,不需要消 debugging 觉。
让我们考虑一个UnaryOperator反转硬币:

UnaryOperator<Coin> coinFlipper = coin -> coin.reverse(); // UnaryOperator requires one argument
UnaryOperator<Coin> coinFlipper1 = Coin::reverse;         // UnaryOperator still requires one argument expressed as a reference to no arg method

UnaryOperator的所有实现都接收一个Coin示例作为参数,并且由于reverse()的调用而产生另一个硬币,reverse是无参数的这一事实不是问题,因为我们关心的是它产生什么,而不是消耗什么。
让我们尝试定义一个更严格的方法引用,首先在Coin类中引入一个新的示例方法xor(),它对于XOR-ing两枚硬币非常有用:

public Coin xor(Coin other) {
    return new Coin(isHeads ^ other.isHeads);
}

现在,当两个对象开始工作时,我们有了更多的可能性,让我们从最简单的情况开始,定义一个UnariOperator

final Coin baseCoin = new Coin();
UnaryOperator<Coin> xorAgainstBase = coin -> baseCoin.xor(coin);
UnaryOperator<Coin> xorAgainstBase1 = baseCoin::xor;

在上面的例子中,在函数外部定义的Coin示例用于通过instance-method执行转换。
稍微复杂一点的情况是BinaryOperator对几个硬币进行异或运算,如下所示:

BinaryOperator<Coin> xor = (coin1, coin2) -> coin1.xor(coin2);
BinaryOperator<Coin> xor1 = Coin::xor;

现在,我们有两个参数作为输入,并且根据BinaryOperator的约定,Coin示例应该作为输出生成。
有趣的是,第一个参数充当了一个示例,xor()方法将在该示例上被调用,第二个参数被传递给该方法(注意xor()只需要一个参数)。
你可能会问,如果有另一种方法对硬币进行异或运算,static方法需要两个参数:

public static Coin xor(Coin coin1, Coin coin2) {
    return new Coin(coin1.isHeads ^ coin2.isHeads);
}

那么编译器将无法解析方法引用,因为这里我们有多个可能适用的方法,并且由于参数类型相同,它们中没有一个可以被认为比另一个更具体,这将导致编译错误,但如果我们有其中一个(而不是两个一起),引用Coin::xor将工作正常。

方法引用类型

基本上,我们已经走过的例子涵盖了所有类型的方法引用。现在,让我们列举它们。
official tutorial provided by Oracle re是四种方法引用:
1.* * 引用静态方法**

Class::staticMethod

引用静态方法xor(Coin coin1, Coin coin2)的示例Coin::xor
标准JDK类的示例:

BinaryOperator<Integer> sum = Integer::sum; // (i1, i2) -> Integer.sum(i1, i2)

BiFunction<CharSequence, Iterable<CharSequence>, String> iterableToString
    = String::join; // (delimiter, strings) -> String.join(delimiter, strings)

1.* * 引用特定对象的示例方法**

instance::instanceMethod

举例说明这种情况的例子是示例方法xor(Coin other)的用法,其中在函数外部定义了一个coin,该coin在内部用于调用xor(),并将函数参数传递到该方法中。

final Coin baseCoin = new Coin();
UnaryOperator<Coin> xorAgainstBase1 = baseCoin::xor; // same as coin -> baseCoin.xor(coin)

标准JDK类的示例:

Set<Foo> fooSet = // initialing the Set
Predicate<Foo> isPresentInFooSet = fooSet::contains;

1.* * 对特定类型的任意对象的示例方法的引用**

Class::methodName

在这种情况下,方法refernce对一个作为参数出现的示例进行操作(只有当我们使用lambda时才能引用它),因此,* containing type *(可以是实际类型或超类型之一)用于引用该示例。
一个例子是Predicate检查硬币是否显示正面Coin::isHeads
标准JDK类的示例:

Function<List<String>, Stream<String>> toStream = Collection::stream;

List<List<String>> lists = List.of(List.of("a", "b", "c"), List.of("x", "y", "z"));

List<String> strings1 = lists.stream()
    .flatMap(toStream)
    .toList();

// A slightly more complicate example taken from the linked tutorial
// Equivalent lambda: (a, b) -> a.compareToIgnoreCase(b)

String[] stringArray = { "Barbara", "James", "Mary" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

1.* * 引用构造函数**

Class::new

我们已经用下列例子说明了这种情况:

  • Supplier<Coin>引用没有作为Coin::new实现的args构造函数;
  • Function<Boolean, Coin>,它通过传递传入的boolean值(也表示为Coin::new)来使用单参数构造函数。
t98cgbkg

t98cgbkg2#

toUpperCase()方法如何从Function接口实现apply(T t)方法?
方法引用String::toUpperCase具有未绑定的接收器。Java 8: Difference between method reference Bound Receiver and UnBound Receiver则参数T t将是接收器
在你的例子中

public <U, T> Optional<U> map(Function<? super T, ? extends U> mapper) {
...
            return Optional.ofNullable(mapper.apply(value));
        }

通过调用map(String::toUpperCase)
如果value(假设等于"Example String")是接收方,则mapper.apply("Example String");将等于"Example String".toUpperCase();

相关问题