在Data Geekery ,我们喜欢Java。 而且,由于我们真的很喜欢jOOQ的流畅的API和查询DSL ,我们对Java 8将为我们的生态系统带来什么感到非常兴奋。
Java 8星期五
每个星期五,我们都会向您展示一些不错的教程风格的Java 8新功能,这些功能利用了lambda表达式,扩展方法和其他好东西。 您可以在GitHub上找到源代码 。
Java 8的阴暗面
到目前为止,我们已经展示了该新主要版本中令人激动的部分 。 但是也有一些警告。 其中很多。 那件事
- ……令人困惑
- … 错了
- …被省略(目前)
- …被省略(很长时间)
Java主要发行版总是有两个方面。 从好的方面来说,我们获得了许多新功能,大多数人会认为这是过期的 。 其他语言,平台早在Java 5之前就有泛型。其他语言,平台早在Java 8之前就有lambda。但是现在,我们终于有了这些功能。 在通常的古怪Java方式中。
Lambda表达式的引入非常优美。 从向后兼容的角度来看,能够将每个匿名SAM实例编写为lambda表达式的想法非常引人注目。 那么,什么是黑暗面到Java 8?
超载变得更糟
重载,泛型和vararg不是朋友。 我们已经在上一篇文章以及这个Stack Overflow问题中 对此进行了解释 。 在您的奇怪应用程序中,这些可能并非每天都有问题,但对于API设计人员和维护人员而言,这是非常重要的问题。
使用lambda表达式,事情变得“更糟”。 因此,您认为可以提供一些便利的API,重载现有的run()
方法(该方法接受Callable
也接受新的Supplier
类型:
static <T> T run(Callable<T> c) throws Exception {return c.call();
}static <T> T run(Supplier<T> s) throws Exception {return s.get();
}
看起来非常有用的Java 7代码现在是Java 8的一大难题。 因为您不能仅使用lambda参数简单地调用这些方法:
public static void main(String[] args)
throws Exception {run(() -> null);// ^^^^^^^^^^ ambiguous method call
}
倒霉。 您将不得不采用以下任一“经典”解决方案:
run((Callable<Object>) (() -> null));run(new Callable<Object>() {@Overridepublic Object call() throws Exception {return null;}});
因此,尽管总有变通办法,但这些变通办法总是“糟透了”。 即使从向后兼容的角度来看事情不会中断,这也真是令人沮丧。
并非所有关键字都支持默认方法
默认方法是一个不错的补充。 有人可能声称Java最终具有特质 。 其他人显然与该术语脱离了关系,例如Brian Goetz:
向Java添加默认方法的主要目标是“接口演变”,而不是“穷人的特征”。
如lambda-dev邮件列表中所示。
事实是,默认方法与Java中的其他任何东西相比都有很多正交和不规则的特征。 这里有一些批评:
他们不能成为最终的
鉴于默认方法也可以用作API中的便捷方法:
public interface NoTrait {// Run the Runnable exactly oncedefault final void run(Runnable r) {// ^^^^^ modifier final not allowedrun(r, 1);}// Run the Runnable "times" timesdefault void run(Runnable r, int times) {for (int i = 0; i < times; i++)r.run();}
}
不幸的是,以上操作是不可能的,因此第一个重载的便捷方法可能会在子类型中被覆盖,即使这对API设计人员而言毫无意义。
无法使其同步
mm! 用语言难以实现吗?
public interface NoTrait {default synchronized void noSynchronized() {// ^^^^^^^^^^^^ modifier synchronized// not allowedSystem.out.println("noSynchronized");}
}
是的, synchronized
很少使用,就像决赛。 但是,当您有了用例时,为什么不仅仅允许它呢? 是什么使接口方法主体如此特别?
默认关键字
这也许是所有功能中最怪异和最不规则的。 default
关键字本身。 让我们比较一下接口和抽象类:
// Interfaces are always abstract
public /* abstract */ interface NoTrait {// Abstract methods have no bodies// The abstract keyword is optional/* abstract */ void run1();// Concrete methods have bodies// The default keyword is mandatorydefault void run2() {}
}// Classes can optionally be abstract
public abstract class NoInterface {// Abstract methods have no bodies// The abstract keyword is mandatoryabstract void run1();// Concrete methods have bodies// The default keyword mustn't be usedvoid run2() {}
}
如果从头开始重新设计该语言,则可能不需要任何abstract
或default
关键字。 两者都是不必要的。 存在或不存在主体的事实足以使编译器评估方法是否抽象。 即,情况应该如何:
public interface NoTrait {void run1();void run2() {}
}public abstract class NoInterface {void run1();void run2() {}
}
上面会更精简和更常规。 遗憾的是,EG从未真正讨论过default
的用途。 好吧,这是经过辩论的,但是EG从来不想接受这种选择。 我已经尝试过运气,下面的响应 :
我不认为#3是一种选择,因为与方法主体的接口一开始是不自然的。 至少指定“默认”关键字为读者提供了某种语言来说明语言允许方法主体的原因。 就个人而言,我希望接口将保持纯合同形式(不执行),但是我不知道有更好的选择来发展接口。
同样,这是EG的明确承诺,即不承诺Java中的“特征”。 默认方法是实现1-2个其他功能的纯粹必要手段。 他们从一开始就没有精心设计。
其他修饰符
幸运的是,在项目后期,使用了static
修饰符使其成为了规范。 因此现在可以在接口中指定静态方法。 但是由于某种原因,这些方法不需要(也不允许!) default
关键字,它必须是EG完全随机决定的,就像您显然无法在接口中定义static final
方法一样。
虽然可见性修饰符已在lambda-dev邮件列表中进行了讨论 ,但超出了此版本的范围。 也许,我们可以在将来的版本中获得它们。
实际上很少执行默认方法
有些方法在接口上会有合理的默认实现–可能会猜测。 直观地,像List
或Set
这样的collections接口会将它们放在其equals()
和hashCode()
方法上,因为这些方法的协定在接口上定义良好。 它还使用listIterator()
在AbstractList
实现,对于大多数定制列表而言,这是合理的默认实现。
如果对这些API进行改造,以使使用Java 8更加容易实现自定义集合,那就太好了。例如,我可以使我的所有业务对象都实现List
,而不会浪费AbstractList
上的单个基类继承。
但是,可能有一个与向后兼容性相关的令人信服的原因阻止了Oracle的Java 8团队实现这些默认方法。 凡是向我们发送此原因的人都将获得免费的jOOQ标签 !
不是在这里发明的-心态
在lambda-dev EG邮件列表中也多次批评了这一点。 而且在撰写本博客系列文章时 ,我只能确认新的功能接口让人很难记住。 由于这些原因,他们感到困惑:
一些原始类型比其他更平等
与所有其他类型相比, int
, long
, double
基本类型是首选的,因为它们在java.util.function包以及整个Streams API中都具有功能接口。 boolean
是二等公民,因为它仍然把它做成包在一个形式BooleanSupplier
或Predicate
,或者更糟: IntPredicate
。
所有其他原始类型在该区域中实际上并不存在。 即没有针对byte
, short
, float
和char
特殊类型。 尽管满足最后期限的争论无疑是有效的,但这种古怪的现状将使新手很难学习这种语言。
类型不仅仅称为函数
坦白说吧。 所有这些类型都只是“功能”。 没有人真正关心Consumer
, Predicate
, UnaryOperator
等之间的隐式差异。
实际上,当您寻找具有非void
返回值和两个参数的类型时,您可能会调用什么呢? Function2
? 好吧,你错了。 它称为BiFunction
。
这是一个决策树,用于了解您要查找的类型如何被调用:
- 您的函数返回
void
吗? 叫做Consumer
- 您的函数返回
boolean
吗? 这叫做Predicate
- 您的函数返回
int
,long
,double
吗? 它叫做XXToIntYY
,XXToLongYY
,XXToDoubleYY
东西 - 您的函数没有参数吗? 叫做
Supplier
- 您的函数是否接受一个
int
,long
,double
参数? 它称为IntXX
,LongXX
,DoubleXX
东西 - 您的函数有两个参数吗? 叫做
BiXX
- 您的函数是否接受两个相同类型的参数? 叫做
BinaryOperator
- 您的函数返回的类型是否与作为单个参数的类型相同? 叫做
UnaryOperator
- 您的函数是否接受两个参数,其中第一个是引用类型,第二个是原始类型? 它称为
ObjXXConsumer
(只有使用该配置的使用者存在) - 否则:称为
Function
好主啊! 最近,我们当然应该去Oracle Education检查一下Oracle Certified Java Programmer课程的价格是否急剧上涨了……幸运的是,有了Lambda表达式,我们几乎不必记住所有这些类型!
有关Java 8的更多信息
Java 5泛型为Java语言带来了许多很棒的新功能。 但是也有很多与类型擦除有关的警告。 Java 8的默认方法,Streams API和lambda表达式将再次为Java语言和平台带来很多很棒的新功能。 但是我们确信, Stack Overflow很快就会因在Java 8丛林中迷路的困惑程序员而引发问题。
学习所有新功能并非易事,但是新功能(和警告)仍然存在。 如果您是Java开发人员,则最好在有机会的时候立即开始练习。 因为我们还有很长的路要走。
翻译自: https://www.javacodegeeks.com/2014/04/java-8-friday-the-dark-side-of-java-8.html