当前位置: 首页 > 科技观察

关于Java你不知道的十件事

时间:2023-03-14 18:45:26 科技观察

作为Java书呆子,我们更感兴趣的是介绍Java和JVM的概念细节,而不是实际技能。因此,我想向大家推荐LukasEder发表在jooq.org上的原创作品。您从早期就开始使用Java了吗?那你还记得它的过去吗?那时候Java还叫Oak,OO还是个热门话题。C++的人认为Java不可能流行起来,而Applets这种由Java开发的小应用程序仍然备受瞩目。我打赌你不知道我要告诉你的一半。让我们深入了解Java的奥秘。1.没有检查异常这回事!JVM不知道这些东西,只知道Java语句。现在每个人都认为检查异常是一个错误。正如BruceEckel在布拉格GeeCON闭幕时所说,Java之后没有其他语言可以检查异常,甚至Java8在新的StreamAPI中也不再这样做(如果你的Lambda使用IO和JDBC,这实际上有点疼痛)。如何证明JVM不知道已检查的异常?试试下面的代码:publicclassTest{//这里没有throws子句publicstaticvoidmain(String[]args){doThrow(newSQLException());}staticvoiddoThrow(Exceptione){Test.doThrow0(e);}@SuppressWarnings("unchecked")staticvoiddoThrow0(Exceptione)throwsE{throw(E)e;}}这不仅会编译,还会抛出SQLException。你甚至不需要Lombok的@SneakyThrows来做到这一点。您可以更详细地查看这篇文章,或在StackOverflow上阅读它。2.您可以定义仅返回值不同的重载函数。这样的代码不会编译,对吧?类测试{对象x(){返回“abc”;}字符串x(){返回“123”;}}正确的。Java语言不允许在同一个类中“等效地重载”两个方法,忽略潜在的差异,例如抛出或返回类型。请参阅Class.getMethod(String,Class...)的Javadoc。解释如下:请注意,类中可能有多个匹配方法,因为Java语言禁止在一个类中声明多个签名相同但返回类型不同的方法,但Java虚拟机则不然.虚拟机中增加的灵活性可用于实现各种语言功能。例如,协变返回可以用桥接方法实现;桥接方法和重写方法将具有相同的签名但返回类型不同。哇,有道理。事实上,下面的代码隐藏了很多东西:}}查看Child生成的字节码://Methoddescriptor#15()Ljava/lang/String;//Stack:1,Locals:1java.lang.Stringx();0ldc[16]2areturn行号:[pc:0,line:7]局部变量表:[pc:0,pc:3]local:thisindex:0type:Child//方法描述符#18()Ljava/lang/Object;//Stack:1,Locals:1bridgesyntheticjava.lang.Objectx();0aload_0[this]1invokevirtualChild.x():java.lang.String[19]4areturnLinenumbers:[pc:0,line:1]事实上,T在字节码中真的只是Object。这很容易理解。合成桥方法实际上是由编译器生成的,因为Parent.x()签名中的返回类型在实际调用时恰好是Object。没有这个桥接方法引入泛型将不会是二进制兼容的。因此,更改JVM以允许此功能不会那么痛苦(具有允许协方差覆盖所有内容的副作用)聪明,不是吗?你看过语言内部的细节吗?看一看,你会在这里发现更多有趣的东西。3.这些都是二维数组!类测试{int[][]a(){返回新的int[0][];}int[]b()[]{返回新的int[0][];}intc()[][]{返回新的int[0][];}}是的,这是真的。即使您的解析器大脑不能立即理解上述方法的返回类型,它们都是一样的!类似地,还有如下代码片段:classTest{int[][]a={{}};int[]b[]={{}};intc[][]={{}};}你觉得这很疯狂?想象一下在顶部使用JSR-308/Java8类型注释。语法的可能性呈指数级增长!@Target(ElementType.TYPE_USE)@interfaceCrazy{}classTest{@Crazyint[][]a1={{}};int@Crazy[][]a2={{}};int[]@Crazy[]a3={{}};@Crazyint[]b1[]={{}};int@Crazy[]b2[]={{}};int[]b3@Crazy[]={{}};@疯狂的intc1[][]={{}};intc2@Crazy[][]={{}};intc3[]@Crazy[]={{}};}类型注解。看似神秘,其实并不难懂。或者换句话说:当我最后一次提交时,是在我4周的假期之前。对于您来说,以上内容是您在实际使用中发现的。4.条件表达式的特例大多数人可能会想到:Objecto1=true?newInteger(1):newDouble(2.0);等同于:Objecto2;if(true)o2=newInteger(1);elseo2=newDouble(2.0);然而,这种情况并非如此。让我们测试一下。System.out.println(o1);System.out.println(o2);输出结果:1.01可以看出,三元条件运算符会在必要时提升操作数的类型。请注意,只有在必要时才这样做;否则,代码可能会抛出NullPointerException空引用异常:Integeri=newInteger(1);if(i.equals(1))i=null;Doubled=newDouble(2.0);Objecto=true?i:d;//NullPointerException!System.out.println(o);5.你还没有理解复合赋值运算符是不是很奇怪?看看下面两行代码:i+=j;i=i+j;直觉上它们是等价的,对吧?但实际上它们并不等价!JLS解释如下:E1op=E2形式的复合赋值表达式等效于E1=(T)((E1)op(E2)),其中T是E1的类型,E1仅计算一次。太好了,我想引用PeterLawrey在StackOverflow上对这个问题的回答:Examplesofcalculationsusing*=or/=byteb=10;b*=5.7;System.out.println(b);//prints57orbyteb=100;b/=2.5;System.out.println(b);//打印40或charch='0';ch*=1.1;System.out.println(ch);//打印'4'或charch='A';ch*=1.5;System.out.println(ch);//打印'a'现在看到这个了吗?我将在我的应用程序中进行字符串乘法。因为,你明白了……6.随机整数现在有一个更难的谜题。不要寻找答案,看看你能不能自己找到。如果我运行以下程序:for(inti=0;i<10;i++){System.out.println((Integer)i);}...“有时”我得到以下输出:922214548236183391933384这怎么可能??.spoiler...继续回答...好吧,答案就在这里(https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/),这必须通过反射整数缓存重写JDK,然后使用自动装箱和拆箱。不要在家里做这种事!或者,我们应该换一种方式来做这种事情。7.GOTO这是我的最爱之一。Java也有GOTO!尝试输入intgoto=1;会输出:Test.java:44:error:expectedintgoto=1;^这是因为goto是一个未使用的关键字,以防万一......但这不是最令人兴奋的部分。令人兴奋的部分是您可以使用break、continue和label块来实现goto功能:向前跳转:label:{//dostuffif(check)breaklabel;//domorestuff}在字节码中的格式如下:2iload_1[check]3ifeq6//Jumpingforward6..向后跳:label:do{//dostuffif(check)continuelabel;//domorestuffbreaklabel;}while(true);字节码中的格式如下:2iload_1[check]3ifeq96goto2//Jumpingbackward9。.8.Java有类型别名在其他语言(如Ceylon)中,我们可以很方便的为类型定义别名:interfacePeople=>Set;这里生成了People类型,使用它与使用Set:People?p1=null;Set?p2=p1;People?p3=p2;在Java中,我们不能在顶级范围内定义类型别名,但我们可以在类或方法范围内定义类型别名。如果我们不喜欢Integer、Long等名称,但又想使用较短的I和L,很简单:classTest{voidx(Ii,Ll){System.out.println(i.intValue()+","+l.longValue());}}在上面的程序中,Integer在Test类的范围内被赋予了一个像I一样的“别名”。类似地,Long在x()方法“别名”中被赋予了一个像L一样的“别名”。然后我们可以这样调用方法:newTest().x(1,2L);这种技术当然没有受到重视。在这种情况下,Integer和Long都是final类型,即I和L是事实上的别名(赋值兼容性基本上只需要考虑一种可能)。如果我们使用一个非final类型(比如Object),它就是一个泛型类型。这些技巧就够了。现在来看看真正非凡的东西!9.有些类型的关系不确定!好吧,这将是戏剧性的,让我们先喝杯咖啡振作起来。考虑以下两种类型://Ahelpertype。您也可以只使用ListinterfaceType{}classCimplementsType>{}classD

implementsType>>>{现在告诉我,C和D类型到底是什么?它们以类似于java.lang.Enum的方式递归存在(但略有不同)。看一看:publicabstractclassEnum>{...}在上面的描述中,枚举实际上只是语法糖://ThisenumMyEnum{}//IsreallyjustsugarforthisclassMyEnumextendsEnum{...}认识到这一点然后我们开始回头看看上面提到的两种类型。下面的代码会编译成什么?classTest{Typec=newC();Type>d=newD();}很难回答的问题,但RossTate已经回答了。这个问题的答案是不确定的:C是Type?Step0)CStep1)Type>?Step0)D>Step1)Type>>>>Step2)D>>Step3)Type>>>Step4)D>>>Step...(expandforever)in尝试在Eclipse中编译它,它会崩溃!(别担心,我提交了一份BUG报告)让我们深入了解...Java中某些类型的关系是不明确的!如果您对Java中的这种用法感到好奇,请查看RossTate(与AlanLeung和SorinLerner合着)的“在Java的类型系统中使用通配符”,我们还讨论了泛型等。状态相关的子类多态性。10.类型交集Java有一个很奇怪的特性,叫做类型交集。您可以声明一个(泛型)类型,它实际上是两种类型的交集,例如:classTest{}绑定到Test类型实例的泛型类型参数T必须实现Serializable和Cloneable。比如String不满足要求,但是Dete满足://Does'tcompileTests=null;//CompilesTestd=null;Java8中已经使用了这个特性,这个有用吗?几乎没用,但是如果你想让一个lambda表达式成为这种类型,真的没有别的办法了。假设您的方法具有这种疯狂的类型约束:voidexecute(Tt){}并且您想执行它以获取可序列化的Runnable对象。Lambdas和序列化也有点奇怪。Lambdas可序列化:如果Lambda的目标类型和参数类型都是可序列化的,那么您可以序列化Lambda,但即使它们是,它们也不能自动实现Serializable标记接口。你必须投类型。但是当你只是抛出Serializable...execute((Serializable)(()->{}));...然后lambda将不再是Runnable。所以转换成两种类型:execute((Runnable&Serializable)(()->{}));结语一句话总结这篇文章:Java恰好是一门看似神秘的语言,其实不然。