我从 Stack Overflow 上找的了一些高关注度且高赞的问题。这些问题可能平时我们遇不到,但既然是高关注的问题和高点赞的回答说明是被大家普遍认可的,如果我们提前学到了以后不管工作中还是面试中处理起来就会更得心应手。本篇文章是第一周的内容,一共 5 个题目。我每天都会在公众号发一篇,你如果觉得这个系列对你有价值,欢迎文末关注我的公众号。


DAY1.  复合运算符中的强制转换

今天讨论的问题是“符合运算符中的强制转换”。以 += 为例,我编写了如下代码,你可以先考虑下为什么会出现下面这种情况。

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。

其实验证也比较容易,我们看下编译后的 .class 文件就知道做了什么处理。

从 .class 文件可以看出,有两处强制转换。第一处是 i+j 时,由于 j 是 long 类型,因此 i 进行类型提升,强转为 long, 这个过程我们比较熟悉。第二处是我们今天讨论的内容,i+j 的结果强转成了 int 类型。
这里面我们还可以在进一步思考,因为在这个例子中强转可能会导致计算结果溢出,那你可以想想为什么 Java 设计的时候不让它报错呢?
我的猜想是这样的,假设遇到这种情况报错,我们看看会有什么样的后果。比如在 byte 或者 short 类型中使用 += 运算符。

byte b = 1;
b += 1;

按照我们的假设,这里就会报错,因为 i+1 返回的 int 类型。然而实际应用场景中这种代码很常见,因此,假设成立的话,将会严重影响复合赋值运算符的应用范围,最终设计出来可能就是一个比较鸡肋的东西。所以,为了普适性只能把判断交给用户,让用户来保障使用复合赋值运算符不会发生溢出。我们平时应用时一定要注意这个潜在的风险。

原文地址


DAY2.  生成随机数你用对了吗

在 Java 中如何生成一个随机数?如果你的答案是 Random 类,那就有必要继续向下看了。Java 7 之前使用 Random 类生成随机数,Java 7 之后的标准做法是使用 ThreadLocalRandom 类,代码如下:

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 类,因此不会对你现有的代码造成很大的影响。

原文地址


DAY3.  InputStream转String有多少种方法

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 这个需求来说却是最方便、最省事的。其实我们平时编程也是一样,对于一个产品需求有时候不需要暴露太多的开放性的选择,针对需求提供一个简单粗暴的实现方式也许是最佳选择。
最后补充一句,我们平时可以多关注框架,用到的时候直接拿过来省时省力,减少代码量。当然有兴趣的话我们也可以深入学习框架内部的设计和实现。

原文地址


DAY4.  面试官:写个内存泄漏的例子

我们都是知道 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 的对象就没有任何变量引用它,因此之前的对象会被回收。

原文地址


DAY5.  为什么密码用 char[] 存储而不用String

周五,放松一下。一起来看一个无需写代码的问题“为什么 Java 程序中用 char[] 保存密码而不用 String”。既然提到密码,我们用脚指头想想也知道肯定是出于安全性的考虑。具体的是为什么呢?我这里提供两点答案供你参考。

先说第一点,也是最重要的一点。String 存储的字符串是不可变的,也就是说用它存储密码后,这块内存是无法被人为改变的。并且只能等 GC 将其清除。如果有其他进程恶意将内存 dump 下来,就可能会造成密码泄露。
然而使用 char[] 存储密码对我们来说就是可控的,我们可以在任何时候将 char[] 的内容设置为空或者其他无意义的字符,从而保证密码不会长期驻留内存。相对使用 String 存储密码来说更加安全。

再说说第二点,假设我们在程序中无意地将密码打印到日志中了。如果使用 String 存储密码将会被明文输出,而使用 char[] 存储密码只会输出地址不会泄露密码。
这两点都是从安全性的角度出发。

第一点更侧重防止密码驻留内存不安全,第二点则侧重防止密码驻留外存。虽然第二点发生的概率比较低,但也给了我们一个新的视角。

以上便是 Stack Overflow 的第一周周报,希望对你有用,后续会继续更新,如果想看日更内容欢迎关注公众号。

欢迎关注公众号「渡码」,分享更多高质量内容

StackOverflow 周报 - 这些高关注的问题你是否都会的更多相关文章

  1. StackOverflow 周报 - 与高关注的问题过过招(Java)

    本篇文章是 Stack Overflow 周报的第二周,共收集了 4 道高关注的问题和对应的高赞回答.公众号「渡码」为日更,欢迎关注. DAY1.  serialVersionUID 的重要性 关注: ...

  2. StackOverflow 周报 - 第四周高质量问题的问答(Java、Python)

    这是 Stack Overflow 第三周周报,由于本周周四外出,所以只有三篇内容.两篇 Java.一篇 Python.公众号「渡码」为日更,欢迎关注. DAY1. 枚举对象 == 和 equals ...

  3. StackOverflow 周报 - 高质量问题的问答(Java、Python)

    这是 Stack Overflow 第三周周报,本周加入了 Python 的内容,原计划两篇 Java.两篇 Python.但明天过节所以今天就先把周报发了,两篇 Java.一篇 Python.公众号 ...

  4. 【Python + Selenium】Mock Testing 是啥?一个so上的高票答案。

    There are many kinds of testing which really made me confused. To be honest, I've never heard of som ...

  5. Twitter 高并发高可用架构

    解决 Twitter的“问题”就像玩玩具一样,这是一个很有趣的扩展性比喻.每个人都觉得 Twitter很简单,一个菜鸟架构师随便摆弄一下个可伸缩的 Twitter就有了,就这么简单.然而事实不是这样, ...

  6. Nginx:论高并发,在座各位都是渣渣

    NGINX 在网络应用中表现超群,在于其独特的设计.许多网络或应用服务器大都是基于线程或者进程的简单框架,NGINX突出的地方就在于其成熟的事件驱动框架,它能应对现代硬件上成千上万的并发连接. NGI ...

  7. (转)MySQL高可用解决方案

    MySQL高可用解决方案 原文:http://www.ywnds.com/?p=5565 有这么两个概念,数据库的可靠性和数据库的可用性,可靠性指的是数据可靠,而可用性指的是服务可用.但是不管是可靠性 ...

  8. 前端工作面试问题--摘取自github

    前端工作面试问题 本文包含了一些用于考查候选者的前端面试问题.不建议对单个候选者问及每个问题 (那需要好几个小时).只要从列表里挑选一些,就能帮助你考查候选者是否具备所需要的技能. 备注: 这些问题中 ...

  9. (译文)Python中的staticmethod与classmethod

    原文是stackoverflow的一则高票回答,原文链接 可能之前也有人翻译过,但是刚好自己也有疑惑,所以搬运一下,个人水平有限所以可能翻译存在误差,欢迎指正(如侵删). 尽管classmethod和 ...

随机推荐

  1. 手动创建MySQL服务

    1.复制一份MySQL服务文件,放入一个路径 2.清理data文件夹下文件,仅保留mysql等 3.修改my.ini,port,...dir等配置 4.管理员运行cmd,cd>bin:mysql ...

  2. MySQL8.0 zip压缩包版本 Windows下安装

    MySQL zip压缩包版本 Windows下安装 Download MySQL Community Server 解压到相应的目录 我的解压目录:D:\Program Files\mysql-8.0 ...

  3. iis8 php-cgi.exe - FastCGI 进程意外退出 500错误解决办法

    今天iis服务环境下的网站突然显示200错误php-cgi.exe - FastCGI 进程意外退出,昨天还好好的网站正常,这个问题一直偶尔出现几次,不是很频繁,但是偶尔会出现: 这是由于某些加载库加 ...

  4. PC端触底效果反复触发的解决方案

    最近在做一个PC端的项目,要求是在滑动到页面的底部的时候就动态的加载下一页的数据,代码实现思路如下: 首先,我们需要知道浏览器中有三个高度,分别是屏幕高度(outerHeight),文档容器高度(in ...

  5. 使用Java实现数据库编程 项目(宠物商店)

    创建数据库代码: DROP DATABASE IF EXISTS petShop; CREATE DATABASE petShop; USE petShop; /*创建表*/ CREATE TABLE ...

  6. Redis(六)--- Redis过期策略与内存淘汰机制

    1.简述 关于Redis键的过期策略,首先要了解两种时间的区别,生存时间和过期时间: 生存时间:一段时长,如30秒.6000毫秒,设置键的生存时间就是设置这个键可以存在多长时间,命令有两个 expir ...

  7. java连接oracle数据库jdbc

    driver = oracle.jdbc.driver.OracleDriver url = jdbc:oracle:thin:@localhost:1521:orcl

  8. 技巧:结合Zabbix与SNMP监控嵌入式设备

    在如何利用Zabbix监控网络设备三篇文章的前两篇中,我们介绍了如何通过Zabbix代理监控网络设备.但有些设备无法安装Zabbix代理,需要采用其他方法监控.需要考虑无法安装软件的嵌入式设备或应用程 ...

  9. vue中使用vue-amap(高德地图)

    因为项目要求调用高德地图,就按照官方文档按部就班的捣鼓,这一路上出了不少问题. 前言: vue-cli,node环境什么的自己安装设置推荐一个博客:https://blog.csdn.net/wula ...

  10. Qt实现表格树控件-自绘树节点虚线

    目录 一.开心一刻 二.自绘树节点? 三.效果展示 四.实现思路 1.可扩展接口 2.函数重写 3.同步左侧表头 五.相关文章 原文链接:Qt实现表格树控件-自绘树节点虚线 一.开心一刻 一程序员第一 ...