文章目录
1. 使用 Long.parse() 时报错
项目重构,测试报出错误 For input string: "100.00"
。因为日志只打印了报错的 message, 没有打印异常类型,看上去有点摸不着头脑。初步分析日志,大致猜测是解析时出了问题,此后使用以下代码本地重现了该问题
String a = "100.0";
Long b = Long.parse(a);
报错 java.lang.NumberFormatException: For input string: "100.00"
显而易见,就是数字解析的时候出现异常。Long 是整形,parse 的数字字符串必须是整形,而被解析的字符串带上了小数点,故解析时抛出了异常
对于这个错误,可以先把 a = "100.0" 使用精度更高的Double或BigDecimal来 parse, 之后取其 Long 值即可
2. YYYY-MM-dd 造成的日期错误
最近遇到个日期格式化的问题,在 Java 中使用以下代码格式化当前时间 2019-12-30
的时候发现最终拿到的字符串是2020-12-30
,问题是显而易见的。查找资料,最终发现该问题和日期格式化的 Pattern
模式有关,大写的 Y
代表的是 week year
,即当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,那么这周就算入下一年,2019-12-30
被算入了下一年的第一周,故年份变成了 2020 年。要解决这个问题,只要将Pattern
模式修改为 yyyy-MM-dd
即可
LocalDate localDate = LocalDate.now();
DateTimeFormatter yyyymMdd = DateTimeFormatter.ofPattern("YYYY-MM-dd");
String format = yyyymMdd.format(localDate);
3. 获取本机 IP 的问题
Java 提供了获取本机 IP 的接口,一个简便的使用如 InetAddress.getLocalHost().getHostAddress()
。但是这种用法会产生一个问题,那就是在 Linux
服务器中获取的本机 IP 可能会是 127.0.0.1
这种没有任何意义的地址。为了适配多种环境,Java 中一个通用的获取本机 IP 方式如下。需注意,要通过网卡名称( Linux 系统 eth0
,MacOS 系统 en0
)过滤一下,不然当本地安装了 docker 容器的时候可能会获取到 docker 的 IP
private String getLocalIp() {
StringBuilder ip = new StringBuilder();
try {
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
NetworkInterface ntf = en.nextElement();
if (!ntf.getName().equals("eth0")) {
continue;
}
for (Enumeration<InetAddress> enumIpAddr = ntf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress()
&& !inetAddress.isLinkLocalAddress() && inetAddress.isSiteLocalAddress()) {
ip.append(inetAddress.getHostAddress());
}
}
}
} catch (SocketException ignored) {
}
return ip.toString();
}
4. IO 中的 readLine() 问题
4.1 网络模式
- 网络 IO 时
readLine()
是阻塞模式,也就是说readLine()
读不到数据的话,会一直阻塞,只有在数据流发生异常或者另一端被close()
掉时,才会返回null
。另外要注意如果用while (br.readLine()!=null)
来判断是否读取到数据会造成数据的漏读,因为这时候已经读了一行,之后没法再获取到这一行,所以应该用while ((line = br.readLine())!=null)
,将读到的数据存储下来 readLine()
通过字符 换行 (\n
)、回车 (\r
) 或 换行回车(/r/n
) 认为某行已终止 ,所以在发送数据的时候要在后面加上这些标志符,否则 readLine() 判断数据传输未完成,程序会一直阻塞等待
4.2 文件读取模式
readLine()
读取到文件的结尾时会返回 null
,因为文件结束数据流也就结束了,这和网络模式是不一样的。如果不指定 buffer 大小,则readLine()
使用的 buffer 有 8192 个字符,在达到 buffer 大小之前,只有遇到\n
、\r
、/r/n
才会返回
5. System.out.println() 的同步特性
如下代码定义一个 boolean
型的 flag
并设置为 false
,主线程一直循环,直到 flag
变为 true
。代码里看起来是在子线程休眠 100ms 后,把 flag
修改为 true
,但需注意这个程序其实是一个死循环,不会正常结束。因为 flag
变量不是被 volatile
修饰的,所以子线程对其的修改不一定能被主线程读到,也就是无法保证可见性
public class VolatileExample {
private static boolean flag = false;
private static int i = 0;
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(100);
flag = true;
System.out.println("flag 被修改成 true");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
while (!flag) {
i++;
// 加上以下打印语句则能正常结束
// System.out.println("flag:" + flag)
}
System.out.println("程序结束,i=" + i);
}
但是当代码中 17 行取消注释,打印 flag
变量时,程序又能正常终止了。查看 println()
方法源码就会发现这个方法内部其实是使用了 synchronized
同步代码块,而线程释放锁的操作会强制性的将工作内存中涉及的共享变量都刷新到主内存中去,这就保证了可见性
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
6. 不调用构造方法创建对象的一种方式
通过 Unsafe#allocateInstance()
方法可以完成一个对象的创建,不过这种方式只会分配 Object
需要的内存空间,而不会触发构造函数,关于Unsafe
具体可参考 Java UnSafe 类使用介绍
Class 的初始化方法
<clinit>
仍然会执行
@Test
public void test() {
Unsafe unsafe = getUnsafe();
try {
DirectMemory directMemory = (DirectMemory) unsafe.allocateInstance(DirectMemory.class);
System.out.println(directMemory.getClass().getClassLoader());
directMemory.setName("HHH");
System.out.println(directMemory.getName());
} catch (InstantiationException e) {
e.printStackTrace();
}
}
// 获取 UnSafe 类实例
public static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return unsafe;
}