开发莫忘基础,写业务写多了很多基础内容容易忘。这里将寻根溯源,总结Java语言规范和基础类中的一些细节问题。所有关于 Java 语言规范的细节问题,都可以参考 The Java® Language Specification, Java SE 8 Edition (JLS8) .
本文将不断补充。。
小数化为整数
Math.floor(x)
返回小于等于 x 的最接近整数,返回类型为 double
;
Math.round(x)
相当于四舍五入,返回值为 long
或 int
;
Math.ceil(x)
返回大于等于 x 的最接近整数,返回类型为 double
。
静态块与构造块
- 静态块:用
static
申明,JVM 加载类时执行,仅执行一次且优先于 main 函数。
- 构造块:类中直接用
{}
定义,每一次创建对象时执行,相当于往构造器最前面加上构造块的内容(很像往每个构造器那里插了内联函数,构造块就相当于内联函数)。
执行顺序优先级:静态块 > 构造块 > 构造方法。有继承关系时,执行顺序通常是:父类静态块→子类静态块→父类构造块→父类构造方法→子类构造块→子类构造方法。
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class test { public static void main(String[] args) { new Derived(); } } class Base { static { System.out.println("fucking => Base::static"); } { System.out.println("fucking => Base::before"); } public Base() { System.out.println("Base::Base<init>"); } } class Derived extends Base { static { System.out.println("fucking => Derived::static"); } { System.out.println("fucking => Derived::before"); } public Derived() { super(); System.out.println("Derived::Derived<init>"); } }
|
输出:
1 2 3 4 5 6
| fucking => Base::static fucking => Derived::static fucking => Base::before Base::Base<init> fucking => Derived::before Derived::Derived<init>
|
运算符规则 - 加法规则
代码片段:
1 2 3 4 5
| byte b1 = 1, b2 = 2, b3, b6; final byte b4 = 4, b5 = 6; b6 = b4 + b5; b3 = (b1 + b2); System.out.println(b3 + b6);
|
结果:第四行编译错误。
表达式的数据类型自动提升, 关于类型的自动提升,注意下面的规则。
- 所有的
byte
,short
,char
型的值将被提升为int
型
- 如果有一个操作数是
long
型,计算结果是long
型
- 如果有一个操作数是
float
型,计算结果是float
型
- 如果有一个操作数是
double
型,计算结果是double
型
而声明为 final
的变量会被 JVM 优化,因此第三句在编译时就会优化为 b6 = 10
,不会出现问题。
float x 与“零值”比较的if语句
float类型的还有double类型的,这些小数类型在趋近于0的时候不会直接等于零,一般都是无限趋近于0。因此不能用==来判断。应该用|x-0| < err
来判断,这里|x-0|表示绝对值,err表示限定误差,用程序表示就是fabs(x) < 0.00001f
。
关于 try 和 finally
- 首先执行到
try
里的 return
,但是有 finally
语句还要执行,于是先执行 return
后面的语句,例如(x++
),把要返回的值保存到局部变量。
- 执行
finally
语句的内容,其中有 return
语句,这时就会忽略 try 中的 return
,直接返回。
返回值问题。可以认为 try
(或者catch
)中的 return
语句的返回值放入线程栈的顶部:如果返回值是基本类型则顶部存放的就是值,如果返回值是引用类型,则顶部存放的是引用。finally中的 return
语句可以修改引用所对应的对象,无法修改基本类型。但不管是基本类型还是引用类型,都可以被 finally
返回的“具体值”具体值覆盖。
三目运算符的类型转换问题
三目运算符里的类型必须一致,比如下面的代码:
1 2 3 4
| int i = 40; String s1 = String.valueOf(i < 50 ? 233 : 666); String s2 = String.valueOf(i < 50 ? 233 : 666.0); assertEquals(true, s1.equals(s2));
|
结果是测试不通过,这里就涉及到三元操作符的转换规则:
- 如果两个操作数无法转换,则不进行转换,返回
Object
对象
- 如果两个操作数是正常的类型,那么按照正常情况进行类型转换,比如
int => long => float => double
- 如果两个操作数都是字面量数字,那么返回范围较大的类型
Java 中自增操作符的一些陷阱
观察下面的一段代码:
1 2 3 4 5 6 7 8 9 10
| public class AutoIncTraps { public static void main(String[] args) { int count = 0; for(int i = 0; i < 10; i++) { count = count++; } System.out.println(count); } }
|
这段代码的打印结果是0
,也就是说自增在这里并没有什么卵用,这和C++是不一样的。反编译一下看一下字节码(main函数部分):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public static main([Ljava/lang/String;)V L0 LINENUMBER 6 L0 ICONST_0 ISTORE 1 L1 LINENUMBER 7 L1 ICONST_0 ISTORE 2 L2 FRAME APPEND [I I] ILOAD 2 BIPUSH 10 IF_ICMPGE L3 L4 LINENUMBER 8 L4 ILOAD 1 IINC 1 1 ISTORE 1 L5 LINENUMBER 7 L5 IINC 2 1 GOTO L2 L3 LINENUMBER 10 L3 FRAME CHOP 1 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; ILOAD 1 INVOKEVIRTUAL java/io/PrintStream.println (I)V L6 LINENUMBER 11 L6 RETURN
|
这里相当于创建了一个局部变量存放count++
,但没有返回,因此count
相当于没变。看了字节码后可能没感觉,写一下编译器处理后的代码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class AutoIncTraps { public AutoIncTraps() { } public static void main(String[] args) { byte count = 0; for(int i = 0; i < 10; ++i) { int var3 = count + 1; count = count; } System.out.println(count); } }
|
总结一下这里count
的处理流程:
- JVM把count值(其值是0)拷贝到临时变量区。
- count值加1,这时候count的值是1。
- 返回临时变量区的值,注意这个值是0,没有修改过。
- 返回值赋值给count,此时count值被重置成0。
单纯看这一个的字节码比较抽象,来看一下这三句的字节码,比较一下更容易理解:
1 2 3
| count = ++count; count = count++; count++;
|
字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| L4 LINENUMBER 9 L4 IINC 1 1 ILOAD 1 ISTORE 1 L5 LINENUMBER 10 L5 ILOAD 1 IINC 1 1 ISTORE 1 L6 LINENUMBER 11 L6 IINC 1 1
|
另外,自增操作不是原子操作,在后边总结并发编程的时候会涉及到。
instanceof 操作符的注意事项
instanceof
操作符左右两边的操作数必须有继承或派生关系,否则不会编译成功。因此,instanceof
操作符只能用于对象,不能用于基本类型(不会自动拆包)。
下面是一些典型的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class FuckingIOF { @Test public void test() { List<Object> list = new ArrayList<>(); list.add("String" instanceof Object); list.add(new String() instanceof Object); list.add(new Object() instanceof String); list.add(null instanceof String); list.add((String)null instanceof String); list.add(null instanceof Object); list.add(new Generic<String>().isDataInstance("")); list.forEach(System.out::println); } } class Generic<T> { public boolean isDataInstance(T t) { return t instanceof Date; } }
|
运行结果和分析:
1 2 3 4 5 6 7
| true => String 是 Object 的子类 true => 同上 false => 同上 false => Java 语言规范规定 null instanceof ? 都是 false false => 同上,无论怎么转换还是 null false => 同上 false => 由于 Java 泛型在编译时会进行类型擦除,因此这里相当于 Object instanceof Date 了
|
诡异的 NaN 类型
根据 JLS8 4.2.3,对 NaN
有以下规定:
- The numerical comparison operators < , <= , > , and >= return false if either or both operands are NaN (§15.20.1).
- The equality operator == returns false if either operand is NaN.
- In particular, (x=y) will be false if x or y is NaN.
- The inequality operator != returns true if either operand is NaN (§15.21.1).
- In particular, x!=x is true if and only if x is NaN.
注意到 Double.NaN == Double.NaN
返回 false,这其实是遵循了 IEEE 754 standard。NaN 代表一个非正常的数(比如除以 0 得到的数),其定义为:
1 2 3 4 5 6
| * A constant holding a Not-a-Number (NaN) value of type * {@code double}. It is equivalent to the value returned by * {@code Double.longBitsToDouble(0x7ff8000000000000L)}. */ public static final double NaN = 0.0d / 0.0;
|
Integer 类的 valueOf 和 parseInt 的对比
这个问题是在 StackOverflow 上看到的。以下三个表达式:
1 2 3
| System.out.println(Integer.valueOf("127") == Integer.valueOf("127")); System.out.println(Integer.valueOf("128") == Integer.valueOf("128")); System.out.println(Integer.parseInt("128") == Integer.valueOf("128"));
|
结果分别是:
为什么是这样的结果呢?我们看一下 valueOf
方法的源码:
1 2 3 4 5 6 7 8 9
| public static Integer valueOf(String s) throws NumberFormatException { return Integer.valueOf(parseInt(s, 10)); } public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
|
可以看到 valueOf
方法是在 parseInt
方法的基础上加了一个读取缓存的过程。我们再看一下 IntegerCache
类的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| * Cache to support the object identity semantics of autoboxing for values between * -128 and 127 (inclusive) as required by JLS. * * The cache is initialized on first usage. The size of the cache * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option. * During VM initialization, java.lang.Integer.IntegerCache.high property * may be set and saved in the private system properties in the * sun.misc.VM class. */ private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); assert IntegerCache.high >= 127; } private IntegerCache() {} }
|
原来 JVM 会缓存一部分的 Integer 对象(默认范围为 -128 - 127
),在通过 valueOf
获取 Integer 对象时,如果是缓存范围内的就直接返回缓存的 Integer 对象,否则就会 new 一个 Integer 对象。返回的上限可通过 JVM 的参数 -XX:AutoBoxCacheMax=<size>
设置,而且不能小于 127(参照 JLS 5.1.7)。这样我们就可以解释 Integer.valueOf("127") == Integer.valueOf("127")
为什么是 true 了,因为它们获取的都是同一个缓存对象,而默认情况下 Integer.valueOf("128") == Integer.valueOf("128")
等效于 new Integer(128) == new Integer(128)
,结果自然是 false。
我们再来看一下 parseInt
方法的原型,它返回一个原生 int 值:
1
| public static int parseInt(String s) throws NumberFormatException
|
由于一个原生值与一个包装值比较时,包装类型会自动拆包,因此 Integer.parseInt("128") == Integer.valueOf("128")
就等效于 128 == 128
,结果自然是 true。
Long 类型同样也有缓存。