我從 Stack Overflow 上找的了一些高關注度且高讚的問題。這些問題可能平時咱們遇不到,但既然是高關注的問題和高點讚的回答說明是被你們廣泛承認的,若是咱們提早學到了之後無論工做中仍是面試中處理起來就會更駕輕就熟。本篇文章是第一週的內容,一共 5 個題目。我天天都會在公衆號發一篇,你若是以爲這個系列對你有價值,歡迎文末關注個人公衆號。java
今天討論的問題是「符合運算符中的強制轉換」。以 += 爲例,我編寫了以下代碼,你能夠先考慮下爲何會出現下面這種狀況。程序員
int i = 5; long j = 10; i += j; //正常 i = i+j; //報錯,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 的複合賦值表達式等價於 E1 = (T)((E1) op (E2)), 其中,T 是 E1 的類型。因此,回到本例,i+j 的結果會強制轉換成 int 再賦值給 i。express
其實驗證也比較容易,咱們看下編譯後的 .class 文件就知道作了什麼處理。編程
從 .class 文件能夠看出,有兩處強制轉換。第一處是 i+j 時,因爲 j 是 long 類型,所以 i 進行類型提高,強轉爲 long, 這個過程咱們比較熟悉。第二處是咱們今天討論的內容,i+j 的結果強轉成了 int 類型。
這裏面咱們還能夠在進一步思考,由於在這個例子中強轉可能會致使計算結果溢出,那你能夠想一想爲何 Java 設計的時候不讓它報錯呢?
個人猜測是這樣的,假設遇到這種狀況報錯,咱們看看會有什麼樣的後果。好比在 byte 或者 short 類型中使用 += 運算符。數組
byte b = 1; b += 1;複製代碼
按照咱們的假設,這裏就會報錯,由於 i+1 返回的 int 類型。然而實際應用場景中這種代碼很常見,所以,假設成立的話,將會嚴重影響複合賦值運算符的應用範圍,最終設計出來可能就是一個比較雞肋的東西。因此,爲了普適性只能把判斷交給用戶,讓用戶來保障使用複合賦值運算符不會發生溢出。咱們平時應用時必定要注意這個潛在的風險。安全
原文地址bash
在 Java 中如何生成一個隨機數?若是你的答案是 Random 類,那就有必要繼續向下看了。Java 7 以前使用 Random 類生成隨機數,Java 7 以後的標準作法是使用 ThreadLocalRandom 類,代碼以下:markdown
ThreadLocalRandom.current().nextInt();複製代碼
既然 Java 7 要引入一個新的類取代以前的 Random 類,說明以前生成隨機數的方式存在必定的問題,下面就結合源碼簡單介紹一下這兩個類的區別。
Random 類是線程安全的,若是多線程同時使用一個 Random 實例生成隨機數,那麼就會共享同一個隨機種子,從而存在併發問題致使性能降低,下面看看 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)); }複製代碼
看到代碼並不複雜,其中,隨機種子 seed 是 AtomicLong 類型的,而且使用 CAS 方式更新種子。
接下來再看看 ThreadLocalRandom 類,多線程調用 ThreadLocalRandom.current() 返回的是同一個 ThreadLocalRandom 實例,但它並不存在多線程同步的問題。看下它更新種子的代碼:
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() 達到了 ThreadLocal 的目的,所以不存在線程安全的問題。使用 ThreadLocalRandom 還有個好處是不須要本身 new 對象,使用起來更方便。若是你的項目是 Java 7+ 而且仍在使用 Random 生成隨機數,那麼建議你切換成 ThreadLocalRandom。因爲它繼承了 Random 類,所以不會對你現有的代碼形成很大的影響。
Java 中若是要將 InputStream 轉成 String,你能想到多少種方法?
String str = "測試"; InputStream inputStream = new ByteArrayInputStream(str.getBytes());複製代碼
1. 使用 ByteArrayOutputStream 循環讀取
/** 1. 使用 ByteArrayOutputStream 循環讀取 */ 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 種方式實現,實際還會有更多的方法。簡單總結一下這幾個方法。
第一種和第二種方法使用原始的循環讀取,代碼量比較大。第三和第四種方法使用了 JDK 封裝好的 API 能夠明顯減小代碼量, 同時 Stream API 可讓咱們將代碼寫成一行,更方便書寫。最後使用 IOUtils 工具類(commons-io 庫), 聽名字就知道是專門作 IO 用的,它也提供了兩種方式,第五種框架提供了更加開放,靈活的方式叫作 copy 方法,也就是說除了 copy 到 String 還能夠 copy 到其餘地方。第六種就徹底的定製化,就是專門用來轉 String 的,固然定製化的結果就是不靈活,但對於單純轉 String 這個需求來講倒是最方便、最省事的。其實咱們平時編程也是同樣,對於一個產品需求有時候不須要暴露太多的開放性的選擇,針對需求提供一個簡單粗暴的實現方式也許是最佳選擇。
最後補充一句,咱們平時能夠多關注框架,用到的時候直接拿過來省時省力,減小代碼量。固然有興趣的話咱們也能夠深刻學習框架內部的設計和實現。
咱們都是知道 Java 自帶垃圾回收機制,內存泄漏這事好像跟 Java 程序員關係不大。因此,寫 Java 程序通常會比 C/C++ 程序輕鬆一些。記得前領導寫 C++ 代碼時說過一句話,「寫 C++ 程序必定會漏的,只不過是能不能被發現而已」。因此看來 C/C++ 程序員仍是比較苦逼的,雖然他們常常鄙視 Java 程序員,哈哈~~。
儘管 Java 程序出現出現內存泄漏的可能性較少,但不表明不會出現。若是你哪天去面試,面試官讓你用 Java 寫一個內存泄漏的例子,你有思路嗎?下面我就舉一個內存泄漏的例子。
public final class ClassLoaderLeakExample { static volatile boolean running = true; /** * 1. main 函數,邏輯比較簡單只是建立一個 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 調用 loadAndDiscard 方法 */ static final class LongRunningThread extends Thread { @Override public 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,它在咱們的例子中相當重要。 * ChildOnlyClassLoader 專門用來裝載 LoadedInChildClassLoader 類, * 邏輯比較簡單,讀取 LoadedInChildClassLoader 類的 .class 文件,返回類對象。 */ static final class ChildOnlyClassLoader extends ClassLoader { ChildOnlyClassLoader() { super(ClassLoaderLeakExample.class.getClassLoader()); } @Override protected 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 線程中被調用的方法。 * 該方法建立 ChildOnlyClassLoader 對象,用來裝載 LoadedInChildClassLoader 類,將結果賦值給 childClass 變量, * childClass 調用 newInstance 方法來建立 LoadedInChildClassLoader 對象。 * 每次調用 loadAndDiscard 方法,都會加載一次 LoadedInChildClassLoader 類並建立其對象。 */ static void loadAndDiscard() throws Exception { ClassLoader childClassLoader = new ChildOnlyClassLoader(); Class<?> childClass = Class.forName( LoadedInChildClassLoader.class.getName(), true, childClassLoader); childClass.newInstance(); } /** * 5. 定義 LoadedInChildClassLoader 類 * 該類中定義了一個 moreBytesToLeak 字節數組,初始大小比較大是爲了儘快模擬出內存泄漏的結果。 * 在類的構造方法調用 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); } } }複製代碼
這是完整的例子, 能夠按照註釋中的序號的順序閱讀代碼。最後運行代碼,在 ClassLoaderLeakExample 類所在的目錄下執行如下命令
javac ClassLoaderLeakExample.java
java -cp . ClassLoaderLeakExample複製代碼
運行後會打印 "Running, press any key to stop." 等一分鐘左右就會報內存不足的錯誤 "java.lang.OutOfMemoryError: Java heap space" 。
簡單梳理一下邏輯,loadAndDiscard 方法會不斷地被調用,每次被調用在該方法中都會加載一次 LoadedInChildClassLoader 類,每加載一次類就會建立一個新的threadLocal 和 moreBytesToLeak 屬性。雖然建立的 LoadedInChildClassLoader 對象是局部變量,但退出 loadAndDiscard 方法後該對象仍然不會被回收,由於 threadLocal 保存了該對象的引用,對象保存了對類的引用,而類保存了對類加載器的引用,類加載器反過來保存對它已加載的類的引用。所以雖然退出 loadAndDiscard 方法,該對象對咱們不可見了,可是它永遠不會被回收。隨着每次加載的類愈來愈多,建立的 moreBytesToLeak 愈來愈多而且內存得不到清理,會致使 OutOfMemory 錯誤。
爲了對比你能夠去掉自定義類加載器這個參數,loadAndDiscard 方法中的代碼修改以下:
Class<?> childClass = Class.forName( LoadedInChildClassLoader.class.getName(), true, childClassLoader); //改成: Class<?> childClass = Class.forName( LoadedInChildClassLoader.class.getName());複製代碼
再運行就不會出現 OOM 的錯誤。修改以後,不管 loadAndDiscard 方法被調用多少次都只會加載一次 LoadedInChildClassLoader 類,也就是說只有一個 threadLocal 和 moreBytesToLeak 屬性。當再次建立 LoadedInChildClassLoader 對象時,threadLocal 會設置成當前的對象,以前 set 的對象就沒有任何變量引用它,所以以前的對象會被回收。
週五,放鬆一下。一塊兒來看一個無需寫代碼的問題「爲何 Java 程序中用 char[] 保存密碼而不用 String」。既然提到密碼,咱們用腳指頭想一想也知道確定是出於安全性的考慮。具體的是爲何呢?我這裏提供兩點答案供你參考。
先說第一點,也是最重要的一點。String 存儲的字符串是不可變的,也就是說用它存儲密碼後,這塊內存是沒法被人爲改變的。而且只能等 GC 將其清除。若是有其餘進程惡意將內存 dump 下來,就可能會形成密碼泄露。
然而使用 char[] 存儲密碼對咱們來講就是可控的,咱們能夠在任什麼時候候將 char[] 的內容設置爲空或者其餘無心義的字符,從而保證密碼不會長期駐留內存。相對使用 String 存儲密碼來講更加安全。
再說說第二點,假設咱們在程序中無心地將密碼打印到日誌中了。若是使用 String 存儲密碼將會被明文輸出,而使用 char[] 存儲密碼只會輸出地址不會泄露密碼。
這兩點都是從安全性的角度出發。
第一點更側重防止密碼駐留內存不安全,第二點則側重防止密碼駐留外存。雖然第二點發生的機率比較低,但也給了咱們一個新的視角。
以上即是 Stack Overflow 的第一週週報,但願對你有用,後續會繼續更新,若是想看日更內容歡迎關注公衆號。
歡迎關注公衆號「渡碼」,分享更多高質量內容