
原文整理了装箱拆箱、重载重写、equals/hashCode和三种字符串类。这几个知识点值得学但表格里有一些容易背成绝对规律的话128一定不缓存、重载只能发生在同一个类、String每次操作都创建新对象、多线程直接用StringBuffer。这次不增加一堆新的名词先用五个能独立运行的小程序看看这些话哪里成立、哪里不成立。实验环境是Windows上的Amazon Corretto17.0.12每段Java源码按public类名保存为同名.java文件然后用javac编译、java运行。输出代表这个环境的结果不是所有JVM实现的性能承诺。1. 自动装箱128为什么不一定比较为false装箱是把基本类型转换成对应包装类型拆箱是取出包装对象里的基本类型值。以Integer为例通常对应Integer.valueOf和intValue。publicclassBoxingCase{publicstaticvoidmain(String[]args){Integera127,b127;Integerx128,y128;System.out.println(127 identity(ab));System.out.println(128 identity(xy));System.out.println(128 valuex.equals(y));System.out.println(mixed primitive(x128));try{Integernnull;intvaluen;System.out.println(value);}catch(NullPointerExceptionexpected){System.out.println(null unboxingNPE);}}}编译后分别运行javac BoxingCase.javajavaBoxingCasejava-XX:AutoBoxCacheMax1000BoxingCase输出项本机默认配置扩大缓存后127 identitytruetrue128 identityfalsetrue128 valuetruetruemixed primitivetruetruenull unboxingNPENPE同样的源码128的引用比较变了。因为这个HotSpot系实现允许通过该选项扩大Integer缓存范围这个选项不是Java语言通用的配置接口。Integer API保证valueOf缓存-128至127也允许缓存其他范围。因此原文“超出范围每次都创建新对象”的说法不能作为通用结论更不能把各种包装类的规则一概而论。真正该分清的是Integer Integer两个引用是否指向同一个对象 Integer.equals按Integer定义比较整数值 Integer int涉及拆箱再比较数值 null - int没有可拆出的值抛NPE这里127的共享也有前提代码使用装箱/valueOf不是说所有值为127的Integer对象必然是同一个对象。2. 重载和重写先决定调用哪个签名再决定谁来执行原文把重载写成“发生在同一个类中”漏掉继承产生的重载集合。也容易让人误以为运行时会根据子类重新选择最合适的参数类型。publicclassDispatchCase{staticclassParent{Stringchoose(Objectvalue){returnParent:Object;}}staticclassChildextendsParent{OverrideStringchoose(Objectvalue){returnChild:Object;}Stringchoose(Stringvalue){returnChild:String;}}publicstaticvoidmain(String[]args){ParentpnewChild();ChildcnewChild();Objectvalueabc;System.out.println(p.choose(abc));System.out.println(c.choose(abc));System.out.println(c.choose(value));}}实际输出Child:Object Child:String Child:Object第一行最重要p的声明类型是Parent编译期可见的候选只有choose(Object)。运行时对象是Child因此执行Child重写的Object版本而不是跳去参数为String的重载。第二行c的声明类型是Child编译器能看到两个候选String实参选择更具体的String版本。第三行value虽然装着字符串但声明类型是Object编译期选Object版本然后运行时执行Child的重写。编译期接收者和参数的类型 - 选中方法签名 choose(Object) 运行期对象实际是Child - 执行Child重写的同一签名 不重新挑choose(String)这是本文普通实例方法例子的调用模型静态方法、private方法等不要直接套动态分派解释。JLS方法调用规则区别重载重写核心同名但参数签名不同子类提供继承实例方法的新实现候选在哪里可以同类也可以涉及继承涉及可被重写的继承方法返回类型不能只靠返回类型区分重载引用返回类型可协变基本类型需匹配选择发生在何时编译期选签名本例运行期选同签名实现其他限制不等于“任何声明都合法”不能降低访问权限、扩大检查异常范围编译器提示Override错误时先检查参数是否真的一样。equals(User)和equals(Object)不是同一个签名前者很可能只是新增重载。3. equals和hashCode为什么相等对象仍可能被放进HashSet两次对象的比较身份equals由类定义相等关系。Object默认equals也是身份比较String、Integer等则提供自己的值相等规则。重写equals后还得守住hashCode契约equals为truehashCode必须相等hashCode相等却不保证equals为true。哈希用于缩小查找范围不是相等证明。Object契约下面故意制造一个违反契约的类与正确类作对照不依赖“Object默认hashCode碰巧不同”这种不保证的现象。importjava.util.HashSet;importjava.util.Set;publicclassEqualityCase{staticfinalclassBadKey{finalintid;finalinthash;BadKey(intid,inthash){this.idid;this.hashhash;}Overridepublicbooleanequals(Objectother){returnotherinstanceofBadKeyid((BadKey)other).id;}OverridepublicinthashCode(){returnhash;}}staticfinalclassKey{finalintid;Key(intid){this.idid;}Overridepublicbooleanequals(Objectother){returnotherinstanceofKeyid((Key)other).id;}OverridepublicinthashCode(){returnInteger.hashCode(id);}}publicstaticvoidmain(String[]args){BadKeyanewBadKey(7,1),bnewBadKey(7,2);SetBadKeybrokennewHashSet();broken.add(a);broken.add(b);SetKeycorrectnewHashSet();correct.add(newKey(7));correct.add(newKey(7));SetStringcollisionnewHashSet();collision.add(Aa);collision.add(BB);System.out.println(bad equalsa.equals(b));System.out.println(bad set sizebroken.size());System.out.println(correct set sizecorrect.size());System.out.println(collision hash(Aa.hashCode()BB.hashCode()));System.out.println(collision set sizecollision.size());}}实际输出bad equalstrue bad set size2 correct set size1 collision hashtrue collision set size2第一组提醒我们类把两个对象声明为相等却提供不同哈希集合无法替这个类修补契约。第三组说明相同哈希的不同字符串可以同时存在哈希冲突不等于重复。这里Key的id用final固定下来。若作为集合键的字段在加入后又改变相等和哈希依据跟着变化还可能导致后续查找失败不是只要写过两个Override就永远正确。空值比较也要明确规则OK.equals(status)允许status为null两个可能为空的引用可用Objects.equals。不能把“内容比较用equals”误解为所有对象类型都自动实现了你想要的内容相等。4. String不可变不等于每次操作都创建新对象publicclassStringCase{publicstaticvoidmain(String[]args){StringsnewString(abc);System.out.println(concat empty identity(s.concat()s));System.out.println(replace absent identity(s.replace(x,y)s));Stringchangeds.replace(a,A);System.out.println(originals);System.out.println(changedchanged);System.out.println(changed identity(changeds));}}实际输出concat empty identitytrue replace absent identitytrue originalabc changedAbc changed identityfalse不可变的意思是原对象的内容不被改掉。需要改变内容时返回别的结果内容无需变化时某些方法可以直接返回原对象。不能由“不可变”推导出“每次调用都分配”。String API原文把性能排成String最慢、Buffer较快、Builder最快也缺了工作负载固定几段拼接通常最直观不能说它必然比手写Builder慢。在循环中不断把旧结果和新片段拼起来Builder能避免反复产生不断变长的中间结果适合考虑。Builder和Buffer性能比较还受JIT、共享方式、锁和输入规模影响。本轮没有做性能基准不给“快几倍”或无条件排行。String可以被多个线程共享读取不等于某个共享引用变量的“读取、拼接、重新赋值”这一串操作自动线程安全。5. StringBuffer线程安全为什么最终仍可能出现AA想实现“如果为空就追加一个A”。两个方法分别安全不代表length与append合起来不可分割。线程1length看到0 ---- 等待 ---- append A 线程2length看到0 ---- 等待 ---- append A 最后AA length和append各自没有损坏内部数据业务上的“只加一次”却没满足。用CountDownLatch控制交错两线程必须都检查完才继续而不是盼着某次压力测试碰巧重现importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.Future;importjava.util.concurrent.TimeUnit;publicclassBufferCase{staticStringrun(booleanwholeOperationLocked)throwsException{StringBufferbuffernewStringBuffer();CountDownLatchcheckednewCountDownLatch(2);CountDownLatchreleasenewCountDownLatch(1);ExecutorServicepoolExecutors.newFixedThreadPool(2);try{java.util.concurrent.CallableVoidtask()-{if(wholeOperationLocked){synchronized(buffer){if(buffer.length()0)buffer.append(A);}}else{booleanemptybuffer.length()0;checked.countDown();if(!release.await(5,TimeUnit.SECONDS)){thrownewIllegalStateException(release timeout);}if(empty)buffer.append(A);}returnnull;};FutureVoidfirstpool.submit(task);FutureVoidsecondpool.submit(task);if(!wholeOperationLocked){if(!checked.await(5,TimeUnit.SECONDS)){thrownewIllegalStateException(check timeout);}release.countDown();}first.get(5,TimeUnit.SECONDS);second.get(5,TimeUnit.SECONDS);returnbuffer.toString();}finally{release.countDown();pool.shutdownNow();}}publicstaticvoidmain(String[]args)throwsException{System.out.println(separate methodsrun(false));System.out.println(whole operationrun(true));}}实际输出separate methodsAA whole operationA修复不是换一个“更安全的字符串类”而是让检查和修改由同一个锁保护并要求其他竞争代码遵守相同协议。StringBuffer API描述了方法同步保证组合操作的业务边界仍要自己设计。本例整体加锁分支不等待“两线程都检查完成”的屏障锁内等待另一个也要获取同锁的线程会把示例写成死锁。验证程序还用Future获取异常和超时避免后台线程失败却被误判为成功。单线程或每线程独立构建字符串时可用Builder。多个线程最终汇总结果时也可以各自构建再用明确的同步方式合并不必一开始就共享一个Buffer。6. 验证程序怎么设计测试从这篇Markdown中提取五个完整Java代码块按public类名写入独立目录实际编译运行而不是另外写一份看起来相似的程序代替正文测试。装箱程序在默认和扩大缓存两种配置下运行分别核对全部输出。方法分派、集合契约和String程序逐行核对预期输出。Buffer程序重复运行20次每次都核对AA/A并设置超时、收集工作线程异常。这是行为验证不是性能测试也不证明每种JVM都支持缓存选项。通过latch约束交错是为了检验一个具体并发场景不代表穷尽所有线程调度。最后可以把五个实验收成一张检查表遇到的口诀先追问什么128一定不缓存这是语言保证、API保证还是某次运行配置运行时看子类选方法先前编译期到底选中了哪个签名equals相等就能去重hashCode是否遵守同一套相等规则不可变必然创建对象这次调用真的改变了内容吗线程安全类就够了业务要求保护一个方法还是一串操作比起再背一个更长的表格我更希望这些程序能让结论有边界知道什么时候能用知道换个条件为什么就不成立。