我从 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. 2019杭电多校第一场hdu6581 Vacation

    Vacation 题目传送门 update(O(n)) 看了那个O(n)的方法,感觉自己想的那个O(nlogn)的好傻,awsl. 0车最终通过停车线的时候,状态一定是某个车堵住后面的所有车(这个车也 ...

  2. 十五、SQL Server中的事务与锁

    (转载别人的内容,值得Mark) 了解事务和锁 事务:保持逻辑数据一致性与可恢复性,必不可少的利器. 锁:多用户访问同一数据库资源时,对访问的先后次序权限管理的一种机制,没有他事务或许将会一塌糊涂,不 ...

  3. MySql(Windows)

    百度云:链接:http://pan.baidu.com/s/1nvlSzMh      密码:o1cw 官网下载网址:http://dev.mysql.com/downloads/mysql/

  4. TP框架基础(三)

    [系统常量信息] 获取系统常量信息: 如果加参数true,会分组显示: >系统常量信息里经常用到的是user里的路径 > APP_PATH =>string'./shop/' 项目路 ...

  5. 代码审计之metinfo5.1.4

    在include/common.inc.php目录下有这样一段代码 这里是有$$变量覆盖的 拿这个sql查询语句测试一下,只要覆盖$tablepre即可进行sql注入 在后面加上如下代码,方便测试的时 ...

  6. HTTP 400 Bad request 原因

    我在使用httpclient 发送http请求时遇到问题,请求报 400 Bad request.网上都在说下面这两个原因 400 是 HTTP 的状态码,主要有两种形式: 1.bad request ...

  7. Android的简述2

    android提供了三种菜单类型,分别为options menu,context menu,sub menu. options menu就是通过按home键来显示,context menu需要在vie ...

  8. javaweb入门---web服务器与HTTP协议基础

    上文web基础简介了web到底是什么,以及身为Java开发人员需要掌握的地方.本文将解答web服务器是什么,怎么使用?还有关于http协议的基础知识. web服务器 web服务器的大概念很广泛,但是通 ...

  9. jboss反序列化漏洞复现(CVE-2017-7504)

    jboss反序列化漏洞复现(CVE-2017-7504) 一.漏洞描述 Jboss AS 4.x及之前版本中,JbossMQ实现过程的JMS over HTTP Invocation Layer的HT ...

  10. 【Android】Failed to convert @drawable/picture into a drawable

    刚使用 eclipse 遇到了这个问题,图片的效果未显示出来,上网查找后发现这其实不算是问题:重启下工程或 eclipse 就行了. PS: 直接运行工程也可以,不影响效果.