Java 基础技术细节总结

Java 基础技术细节总结

开发莫忘基础,写业务写多了很多基础内容容易忘。

开发莫忘基础,写业务写多了很多基础内容容易忘。这里将寻根溯源,总结 Java 语言规范和基础类中的一些细节问题。所有关于 Java 语言规范的细节问题,都可以参考 The Java® Language Specification, Java SE 8 Edition (JLS8) .

本文将不断补充。。

小数化为整数

  • Math.floor(x) 返回小于等于 x 的最接近整数,返回类型为 double;
  • Math.round(x) 相当于四舍五入,返回值为 longint;
  • Math.ceil(x) 返回大于等于 x 的最接近整数,返回类型为 double

静态块与构造块

  • 静态块:用 static 申明,JVM 加载类时执行,仅执行一次且优先于 main 函数。
  • 构造块:类中直接用 {} 定义,每一次创建对象时执行,相当于往构造器最前面加上构造块的内容(很像往每个构造器那里插了内联函数,构造块就相当于内联函数)。

执行顺序优先级:静态块 > 构造块 > 构造方法。有继承关系时,执行顺序通常是:父类静态块→子类静态块→父类构造块→父类构造方法→子类构造块→子类构造方法

测试:

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>");

        }

    }

输出:

fucking => Base::static

fucking => Derived::static

fucking => Base::before

Base::Base<init>

fucking => Derived::before

Derived::Derived<init>

运算符规则 - 加法规则

代码片段:

byte b1 = 1, b2 = 2, b3, b6;

final byte b4 = 4, b5 = 6;

b6 = b4 + b5;

b3 = (b1 + b2);

System.out.println(b3 + b6);

结果:第四行编译错误。

表达式的数据类型自动提升, 关于类型的自动提升,注意下面的规则。

  1. 所有的byte,short,char型的值将被提升为int型(也就是说,两个byte类型的对象使用运算符计算会被自动提升为int,除非我们手动进行强制类型转换)
  2. 如果有一个操作数是long型,计算结果是long
  3. 如果有一个操作数是float型,计算结果是float
  4. 如果有一个操作数是double型,计算结果是double

而声明为 final 的变量会被 JVM 优化,因此第三句在编译时就会优化为 b6 = 10,不会出现问题。

float x 与 “零值” 比较的 if 语句

if (fabs(x) < 0.00001f)

float 类型的还有 double 类型的,这些小数类型在趋近于 0 的时候不会直接等于零,一般都是无限趋近于 0。因此不能用 == 来判断。应该用|x-0| < err来判断,这里 | x-0 | 表示绝对值,err 表示限定误差,用程序表示就是fabs(x) < 0.00001f

关于 try 和 finally

  1. 首先执行到 try 里的 return,但是有 finally 语句还要执行,于是先执行 return 后面的语句,例如(x++),把要返回的值保存到局部变量。
  2. 执行 finally 语句的内容,其中有 return 语句,这时就会忽略 try 中的 return,直接返回。

返回值问题。可以认为 try(或者catch)中的 return 语句的返回值放入线程栈的顶部:如果返回值是基本类型则顶部存放的就是值,如果返回值是引用类型,则顶部存放的是引用。finally 中的 return 语句可以修改引用所对应的对象,无法修改基本类型。但不管是基本类型还是引用类型,都可以被 finally 返回的 “具体值” 具体值覆盖。

三目运算符的类型转换问题

三目运算符里的类型必须一致,比如下面的代码:

int i = 40;

String s1 = String.valueOf(i < 50 ? 233 : 666);//输出是233

String s2 = String.valueOf(i < 50 ? 233 : 666.0);//输出是233.0

assertEquals(true, s1.equals(s2));//断言这两个是相等的,否则就报错

结果是测试不通过,这里就涉及到三元操作符的转换规则:

  1. 如果两个操作数无法转换,则不进行转换,返回 Object 对象
  2. 如果两个操作数是正常的类型,那么按照正常情况进行类型转换,比如int => long => float => double
  3. 如果两个操作数都是字面量数字,那么返回范围较大的类型

Java 中自增操作符的一些陷阱

观察下面的一段代码:

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 函数部分):

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相当于没变。看了字节码后可能没感觉,写一下编译器处理后的代码吧:

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的处理流程:

  1. JVM 把 count 值(其值是 0)拷贝到临时变量区。
  2. count 值加 1,这时候 count 的值是 1。
  3. 返回临时变量区的值,注意这个值是 0,没有修改过。
  4. 返回值赋值给 count,此时 count 值被重置成 0。

单纯看这一个的字节码比较抽象,来看一下这三句的字节码,比较一下更容易理解:

count = ++count;

count = count++;

count++;

字节码:

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 操作符的注意事项

java 中的instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例。instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例。

instanceof 操作符左右两边的操作数必须有继承或派生关系,否则不会编译成功。因此,instanceof 操作符只能用于对象,不能用于基本类型(不会自动拆包)。

下面是一些典型的例子:

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;

    }

}

运行结果和分析:

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 得到的数),其定义为:

* 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 上看到的。以下三个表达式:

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"));

结果分别是:

true

false

true

为什么是这样的结果呢?我们看一下 valueOf 方法的源码:

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 类的源码:

* 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= 设置,而且不能小于 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 值:

public static int parseInt(String s) throws NumberFormatException

由于一个原生值与一个包装值比较时,包装类型会自动拆包,因此 Integer.parseInt("128") == Integer.valueOf("128") 就等效于 128 == 128,结果自然是 true。

Long 类型同样也有缓存。

posted @ 2020-02-26 15:30  别再闹了  阅读(232)  评论(0)    收藏  举报