我從 Stack Overflow
上找的了一些高關(guān)注度且高贊的問題。這些問題可能平時我們遇不到,但既然是高關(guān)注的問題和高點(diǎn)贊的回答說明是被大家普遍認(rèn)可的,如果我們提前學(xué)到了以后不管工作中還是面試中處理起來就會更得心應(yīng)手。本篇文章是第一周的內(nèi)容,一共
5 個題目。我每天都會在公眾號發(fā)一篇,你如果覺得這個系列對你有價值,歡迎文末關(guān)注我的公眾號。
?
DAY1.??復(fù)合運(yùn)算符中的強(qiáng)制轉(zhuǎn)換
今天討論的問題是“符合運(yùn)算符中的強(qiáng)制轉(zhuǎn)換”。以 += 為例,我編寫了如下代碼,你可以先考慮下為什么會出現(xiàn)下面這種情況。
int i = 5; long j = 10; i += j; //正常 i = i+j; //報(bào)錯,Incompatible types.
這個問題可以從 “Java 語言手冊” 中找到答案,原文如下:
A compound assignment expression of the form E1 op= E2 is equivalent to E1 =
(T) ((E1) op (E2)), where T is the type of E1, except that E1 is evaluated only
once.
翻譯一下:形如 E1 op= E2 的復(fù)合賦值表達(dá)式等價于 E1 = (T)((E1) op (E2)), 其中,T 是 E1
的類型。所以,回到本例,i+j 的結(jié)果會強(qiáng)制轉(zhuǎn)換成 int 再賦值給 i。
其實(shí)驗(yàn)證也比較容易,我們看下編譯后的 .class 文件就知道做了什么處理。
從 .class 文件可以看出,有兩處強(qiáng)制轉(zhuǎn)換。第一處是 i+j 時,由于 j 是 long 類型,因此 i 進(jìn)行類型提升,強(qiáng)轉(zhuǎn)為 long,
這個過程我們比較熟悉。第二處是我們今天討論的內(nèi)容,i+j 的結(jié)果強(qiáng)轉(zhuǎn)成了 int 類型。
這里面我們還可以在進(jìn)一步思考,因?yàn)樵谶@個例子中強(qiáng)轉(zhuǎn)可能會導(dǎo)致計(jì)算結(jié)果溢出,那你可以想想為什么 Java 設(shè)計(jì)的時候不讓它報(bào)錯呢?
我的猜想是這樣的,假設(shè)遇到這種情況報(bào)錯,我們看看會有什么樣的后果。比如在 byte 或者 short 類型中使用 +=?運(yùn)算符。
byte b = 1; b += 1;
按照我們的假設(shè),這里就會報(bào)錯,因?yàn)?i+1 返回的 int
類型。然而實(shí)際應(yīng)用場景中這種代碼很常見,因此,假設(shè)成立的話,將會嚴(yán)重影響復(fù)合賦值運(yùn)算符的應(yīng)用范圍,最終設(shè)計(jì)出來可能就是一個比較雞肋的東西。所以,為了普適性只能把判斷交給用戶,讓用戶來保障使用復(fù)合賦值運(yùn)算符不會發(fā)生溢出。我們平時應(yīng)用時一定要注意這個潛在的風(fēng)險(xiǎn)。
原文地址
<https://mp.weixin.qq.com/s?__biz=MjM5MjcwMjk4OA==&mid=2247483770&idx=1&sn=96fd784c94801cbfa0251877b5815357&chksm=a6a3761991d4ff0f3e84786d6f3b2f50f41a95872247b6aaf2d4b5021c971190e271bef02271&token=1451746968&lang=zh_CN#rd>
?
DAY2.??生成隨機(jī)數(shù)你用對了嗎
在 Java 中如何生成一個隨機(jī)數(shù)?如果你的答案是 Random 類,那就有必要繼續(xù)向下看了。Java 7 之前使用 Random 類生成隨機(jī)數(shù),Java
7 之后的標(biāo)準(zhǔn)做法是使用 ThreadLocalRandom 類,代碼如下:
ThreadLocalRandom.current().nextInt();
既然 Java 7 要引入一個新的類取代之前的 Random 類,說明之前生成隨機(jī)數(shù)的方式存在一定的問題,下面就結(jié)合源碼簡單介紹一下這兩個類的區(qū)別。
Random 類是線程安全的,如果多線程同時使用一個 Random 實(shí)例生成隨機(jī)數(shù),那么就會共享同一個隨機(jī)種子,從而存在并發(fā)問題導(dǎo)致性能下降,下面看看
next(int bits) 方法的源碼:
protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this
.seed;do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend) &
mask; }while (!seed.compareAndSet(oldseed, nextseed)); return (int)(nextseed
>>> (48 - bits)); }
看到代碼并不復(fù)雜,其中,隨機(jī)種子 seed 是 AtomicLong 類型的,并且使用 CAS 方式更新種子。
接下來再看看?ThreadLocalRandom 類,多線程調(diào)用 ThreadLocalRandom.current() 返回的是同一個
ThreadLocalRandom 實(shí)例,但它并不存在多線程同步的問題??聪滤路N子的代碼:
final long nextSeed() { Thread t; long r; // read and update per-thread seed
UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) +
GAMMA);return r; }
可以看到,這里面不存在線程同步的代碼。猜測代碼中使用了Thread.currentThread() 達(dá)到了? ThreadLocal
的目的,因此不存在線程安全的問題。使用 ThreadLocalRandom 還有個好處是不需要自己 new 對象,使用起來更方便。如果你的項(xiàng)目是 Java
7+ 并且仍在使用 Random 生成隨機(jī)數(shù),那么建議你切換成 ThreadLocalRandom。由于它繼承了 Random
類,因此不會對你現(xiàn)有的代碼造成很大的影響。
原文地址
<https://mp.weixin.qq.com/s?__biz=MjM5MjcwMjk4OA==&mid=2247483781&idx=1&sn=638629ddbaf0a8bddc2a5c249b972cc0&chksm=a6a376e691d4fff08d8247533aff3e242384193b0eeb9b8acd767f718fe173af8254156f0c5d&token=1451746968&lang=zh_CN#rd>
?
DAY3.??InputStream轉(zhuǎn)String有多少種方法
Java 中如果要將 InputStream 轉(zhuǎn)成 String,你能想到多少種方法?
String str = "測試"; InputStream inputStream = new
ByteArrayInputStream(str.getBytes());
1.?使用 ByteArrayOutputStream 循環(huán)讀取
/** 1. 使用 ByteArrayOutputStream 循環(huán)讀取 */ BufferedInputStream bis = new
BufferedInputStream(inputStream); ByteArrayOutputStream buf= new
ByteArrayOutputStream();int tmpRes = bis.read(); while(tmpRes != -1) {
buf.write((byte) tmpRes); tmpRes = bis.read(); }
System.out.println(buf.toString());
2.?使用 InputStreamReader 批量讀取
/** 2. 使用 InputStreamReader 批量讀取 */ final char[] buffer = new char[1024]; final
StringBuilder out =new StringBuilder(); Reader in = new
InputStreamReader(inputStream);for (; ; ) { int rsz = in.read(buffer, 0,
buffer.length);if (rsz < 0) { break; } out.append(buffer, 0, rsz); }
System.out.println(out.toString());
3.?使用 JDK Scanner
/** 3. 使用 JDK Scanner */ Scanner s = new
Scanner(inputStream).useDelimiter("\\A"); String result = s.hasNext() ?
s.next() : ""; System.out.println(result);
4. 使用 Java 8 Stream API
/** 4. 使用 Java 8 Stream API */ result = new BufferedReader(new
InputStreamReader(inputStream)) .lines().collect(Collectors.joining("\n"));
System.out.println(result);
5. 使用 IOUtils StringWriter
/** 5. 使用 IOUtils StringWriter */ StringWriter stringWriter = new
StringWriter(); IOUtils.copy(inputStream, stringWriter);
System.out.println(stringWriter.toString());
6. 使用 IOUtils.toString 一步到位
/** 6. 使用 IOUtils.toString 一步到位 */
System.out.println(IOUtils.toString(inputStream));
這里我們用了 6 種方式實(shí)現(xiàn),實(shí)際還會有更多的方法。簡單總結(jié)一下這幾個方法。
第一種和第二種方法使用原始的循環(huán)讀取,代碼量比較大。第三和第四種方法使用了 JDK 封裝好的 API 可以明顯減少代碼量, 同時 Stream API
可以讓我們將代碼寫成一行,更方便書寫。最后使用 IOUtils 工具類(commons-io 庫), 聽名字就知道是專門做 IO
用的,它也提供了兩種方式,第五種框架提供了更加開放,靈活的方式叫做 copy 方法,也就是說除了 copy 到 String 還可以 copy
到其他地方。第六種就完全的定制化,就是專門用來轉(zhuǎn) String 的,當(dāng)然定制化的結(jié)果就是不靈活,但對于單純轉(zhuǎn) String
這個需求來說卻是最方便、最省事的。其實(shí)我們平時編程也是一樣,對于一個產(chǎn)品需求有時候不需要暴露太多的開放性的選擇,針對需求提供一個簡單粗暴的實(shí)現(xiàn)方式也許是最佳選擇。
最后補(bǔ)充一句,我們平時可以多關(guān)注框架,用到的時候直接拿過來省時省力,減少代碼量。當(dāng)然有興趣的話我們也可以深入學(xué)習(xí)框架內(nèi)部的設(shè)計(jì)和實(shí)現(xiàn)。
原文地址
<https://mp.weixin.qq.com/s?__biz=MjM5MjcwMjk4OA==&mid=2247483797&idx=1&sn=705bb8d0ea37331c6fa3bf848cda01b6&chksm=a6a376f691d4ffe0bdb635f9dca8a8a0c867a5d3793e7afc1ae06dea6f94b9a0ade594ebcb78&token=1451746968&lang=zh_CN#rd>
?
DAY4.??面試官:寫個內(nèi)存泄漏的例子
我們都是知道 Java 自帶垃圾回收機(jī)制,內(nèi)存泄漏這事好像跟 Java 程序員關(guān)系不大。所以,寫 Java 程序一般會比 C/C++
程序輕松一些。記得前領(lǐng)導(dǎo)寫 C++ 代碼時說過一句話,“寫 C++ 程序一定會漏的,只不過是能不能被發(fā)現(xiàn)而已”。所以看來 C/C++
程序員還是比較苦逼的,雖然他們經(jīng)常鄙視 Java 程序員,哈哈~~。
盡管?Java 程序出現(xiàn)出現(xiàn)內(nèi)存泄漏的可能性較少,但不代表不會出現(xiàn)。如果你哪天去面試,面試官讓你用 Java
寫一個內(nèi)存泄漏的例子,你有思路嗎?下面我就舉一個內(nèi)存泄漏的例子。
public final class ClassLoaderLeakExample { static volatile boolean running =
true; /** * 1. main 函數(shù),邏輯比較簡單只是創(chuàng)建一個 LongRunningThread 線程,并接受停止的指令 */ public
static void main(String[] args) throws Exception { Thread thread = new
LongRunningThread();try { thread.start(); System.out.println("Running, press
any key to stop."); System.in.read(); } finally { running = false;
thread.join(); } }/** * 2. 定義 LongRunningThread 線程,該線程做的事情比較簡單,每隔 100ms 調(diào)用
loadAndDiscard 方法*/ static final class LongRunningThread extends Thread {
@Overridepublic void run() { while(running) { try { loadAndDiscard(); } catch
(Throwable ex) { ex.printStackTrace(); }try { Thread.sleep(100); } catch
(InterruptedException ex) { System.out.println("Caught InterruptedException,
shutting down."); running = false; } } } } /** * 3. 定義一個 class loader -
ChildOnlyClassLoader,它在我們的例子中至關(guān)重要。 * ChildOnlyClassLoader 專門用來裝載
LoadedInChildClassLoader 類, * 邏輯比較簡單,讀取 LoadedInChildClassLoader 類的 .class
文件,返回類對象。*/ static final class ChildOnlyClassLoader extends ClassLoader {
ChildOnlyClassLoader() {super(ClassLoaderLeakExample.class.getClassLoader()); }
@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {if (!LoadedInChildClassLoader.class
.getName().equals(name)) {return super.loadClass(name, resolve); } try { Path
path= Paths.get(LoadedInChildClassLoader.class.getName() + ".class"); byte[]
classBytes = Files.readAllBytes(path); Class<?> c = defineClass(name,
classBytes, 0, classBytes.length); if (resolve) { resolveClass(c); } return c; }
catch (IOException ex) { throw new ClassNotFoundException("Could not load " +
name, ex); } } }/** * 4. 編寫 loadAndDiscard 方法的代碼,也就是在 LongRunningThread
線程中被調(diào)用的方法。 * 該方法創(chuàng)建 ChildOnlyClassLoader 對象,用來裝載 LoadedInChildClassLoader
類,將結(jié)果賦值給 childClass 變量, * childClass 調(diào)用 newInstance 方法來創(chuàng)建
LoadedInChildClassLoader 對象。 * 每次調(diào)用 loadAndDiscard 方法,都會加載一次
LoadedInChildClassLoader 類并創(chuàng)建其對象。*/ static void loadAndDiscard() throws
Exception { ClassLoader childClassLoader= new ChildOnlyClassLoader(); Class<?>
childClass = Class.forName( LoadedInChildClassLoader.class.getName(), true,
childClassLoader); childClass.newInstance(); }/** * 5. 定義
LoadedInChildClassLoader 類 * 該類中定義了一個 moreBytesToLeak
字節(jié)數(shù)組,初始大小比較大是為了盡快模擬出內(nèi)存泄漏的結(jié)果。 * 在類的構(gòu)造方法調(diào)用 threadLocal 的 set 方法存儲對象本身的引用。*/ public
static final class LoadedInChildClassLoader { static final byte[]
moreBytesToLeak =new byte[1024 * 1024 * 10]; private static final
ThreadLocal<LoadedInChildClassLoader> threadLocal = new ThreadLocal<>(); public
LoadedInChildClassLoader() { threadLocal.set(this); } } }
這是完整的例子, 可以按照注釋中的序號的順序閱讀代碼。最后運(yùn)行代碼,在 ClassLoaderLeakExample 類所在的目錄下執(zhí)行以下命令
javac ClassLoaderLeakExample.java java -cp . ClassLoaderLeakExample
運(yùn)行后會打印?"Running, press any key to stop." 等一分鐘左右就會報(bào)內(nèi)存不足的錯誤
"java.lang.OutOfMemoryError: Java heap space" 。
簡單梳理一下邏輯,loadAndDiscard 方法會不斷地被調(diào)用,每次被調(diào)用在該方法中都會加載一次 LoadedInChildClassLoader
類,每加載一次類就會創(chuàng)建一個新的threadLocal 和 moreBytesToLeak 屬性。雖然創(chuàng)建的 LoadedInChildClassLoader
對象是局部變量,但退出 loadAndDiscard ?方法后該對象仍然不會被回收,因?yàn)?threadLocal
保存了該對象的引用,對象保存了對類的引用,而類保存了對類加載器的引用,類加載器反過來保存對它已加載的類的引用。因此雖然退出 loadAndDiscard
方法,該對象對我們不可見了,但是它永遠(yuǎn)不會被回收。隨著每次加載的類越來越多,創(chuàng)建的 moreBytesToLeak 越來越多并且內(nèi)存得不到清理,會導(dǎo)致
OutOfMemory 錯誤。
為了對比你可以去掉自定義類加載器這個參數(shù),loadAndDiscard 方法中的代碼修改如下:
Class<?> childClass = Class.forName( LoadedInChildClassLoader.class.getName(),
true, childClassLoader); //改為: Class<?> childClass = Class.forName(
LoadedInChildClassLoader.class.getName());
再運(yùn)行就不會出現(xiàn) OOM 的錯誤。修改之后,無論 loadAndDiscard 方法被調(diào)用多少次都只會加載一次
LoadedInChildClassLoader 類,也就是說只有一個 threadLocal 和 moreBytesToLeak 屬性。當(dāng)再次創(chuàng)建
LoadedInChildClassLoader 對象時,threadLocal 會設(shè)置成當(dāng)前的對象,之前 set
的對象就沒有任何變量引用它,因此之前的對象會被回收。
原文地址
<https://mp.weixin.qq.com/s?__biz=MjM5MjcwMjk4OA==&mid=2247483812&idx=1&sn=59037988f7a99e4554fc8a414e3948bb&chksm=a6a376c791d4ffd1425425c8bec32d7624997912d988b7cb1de46660a9acbb56de0bcb3af557&token=1451746968&lang=zh_CN#rd>
?
DAY5.??為什么密碼用 char[] 存儲而不用String
周五,放松一下。一起來看一個無需寫代碼的問題“為什么 Java 程序中用 char[] 保存密碼而不用
String”。既然提到密碼,我們用腳指頭想想也知道肯定是出于安全性的考慮。具體的是為什么呢?我這里提供兩點(diǎn)答案供你參考。
先說第一點(diǎn),也是最重要的一點(diǎn)。String 存儲的字符串是不可變的,也就是說用它存儲密碼后,這塊內(nèi)存是無法被人為改變的。并且只能等 GC
將其清除。如果有其他進(jìn)程惡意將內(nèi)存 dump 下來,就可能會造成密碼泄露。
然而使用 char[] 存儲密碼對我們來說就是可控的,我們可以在任何時候?qū)?char[]
的內(nèi)容設(shè)置為空或者其他無意義的字符,從而保證密碼不會長期駐留內(nèi)存。相對使用 String 存儲密碼來說更加安全。
再說說第二點(diǎn),假設(shè)我們在程序中無意地將密碼打印到日志中了。如果使用 String 存儲密碼將會被明文輸出,而使用 char[]
存儲密碼只會輸出地址不會泄露密碼。
這兩點(diǎn)都是從安全性的角度出發(fā)。
第一點(diǎn)更側(cè)重防止密碼駐留內(nèi)存不安全,第二點(diǎn)則側(cè)重防止密碼駐留外存。雖然第二點(diǎn)發(fā)生的概率比較低,但也給了我們一個新的視角。
以上便是 Stack Overflow 的第一周周報(bào),希望對你有用,后續(xù)會繼續(xù)更新,如果想看日更內(nèi)容歡迎關(guān)注公眾號。
歡迎關(guān)注公眾號「渡碼」,分享更多高質(zhì)量內(nèi)容
熱門工具 換一換