Java问题记录


观察一下代码,说出执行结果(JVM类加载)

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
class A{
static {
System.out.print("1");
}

public A(){
System.out.print("2");
}
}

class B extends A{
static {
System.out.print("a");
}

public B(){
System.out.print("b");
}
}

public class AppTest {
public static void main(String[] args) {
A a = new B();
a = new B();
}
}

解析

从JVM加载class文件的原理开始。
JVM中类的装载是由类加载器ClassLoader及其子类来实现的,Java中的类加载器是一个Java运行时系统组件,负责在运行时查找和装载类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)及初始化。

  1. 类的加载是指把类的.class文件中的数据读取到内存中,通常是创建一个字节数组读入.class文件,然后产生所加载类的Class对象。加载完成后,Class对象还不完整,此时的类还不可用。
  2. 进入连接阶段,该阶段又分为3阶段
    1. 验证节点: 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件。验证内容涵盖了类数据信息的格式验证、语义验证、操作验证等。
      1. 文件格式验证:验证是否符合.class文件规范
        1. 语义验证(元数据验证):检查一个被标记为final的类时候包含子类;检查final方法时候被重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但返回值不同)……
          1. 操作验证(字节码验证):保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(比如不应该出现在操作栈中放置了一个int类型的数据,使用时却按long类型来加载如本地变量表中);保证跳转指令不会跳转到方法体以外的字节码指令上……
    2. 准备阶段: 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于未产生对象,实例变量不在此操作范围内),被final修饰的静态变量,会直接赋值原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值。
    3. 解析阶段: 将常量池中的符号引用转为直接引用,得到类或者字段、方法在内存中的地址或者相对偏移量,以便指针直接调用该方法。

什么是符号引用?
就是以一组字面量来描述所引用的目标。是相对于直接引用的。符号引用包括三类常量,分别是类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。

  1. 初始化: 将类中所有被static关键字修饰的代码统一执行一次,如果执行的是静态变量,那么就会使用开发者赋予的值来覆盖先前准备阶段的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作;如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;如果类中存在初始化语句,就依次执行这些初始化语句。

    • 初始化阶段是执行类的构造器<clinit>()方法的过程
      • <clinit>()方法时有编译器自动收集类中所有的类变量赋值动作和静态语句块中的语句合并产生的,收集顺序取决于出现在源文件中的位置。
      • <clinit>()方法与类的构造函数不同,它不需要显式调用父类构造器,虚拟机保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此JVM中第一个被执行的<clinit>()方法一定是java.lang.Object
      • 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
      • 如果一个类没有静态变量或者静态语句块,那么编译器可以不为这个类生成<clinit>()方法
      • 接口中不能使用静态代码块,但是可以使用静态变量。与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有父接口中定义的变量被使用时父接口才会被初始化,另外,接口的实现类在初始化时也不会执行接口的<clinit>()方法
      • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步,当一个线程访问执行初始化操作时,其余线程必须等待,只有执行完对类的初始化操作之后,才会通知等待的其他线程。

答案:1a2b2b

以下代码的输出结果是?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class B{
public static B t1 = new B();
public static B t2 = new B();
{
System.out.println("构造块");
}
static {
System.out.println("静态块");
}
public static void main(String[] args){
B t = new B();
}
}

解析

静态域:用static声明,JVM加载类时执行,仅执行一次。

构造代码块:类中直接用{}定义,每一次创建对象时执行。

执行顺序优先级:静态域 > main() > 构造代码块 > 构造方法

  1. 静态域:第一个静态域是一个静态变量public static B t1 = new B();创建对象,会执行构造代码块,所以先输出构造块,然后执行第二个静态变量初始化,即public static B t2 = new B();创建对象,同样也会执行构造代码块,所以会输出构造块。接着执行下一个静态代码块static {...},输出静态块
  2. main():B t = new B();执行,创建对象,只会执行构造代码块,输出构造块
  3. 构造代码块:构造代码块只会在创建对象时执行,没有继续创建对象,所以没输出
  4. 构造方法:使用默认构造函数,所以没输出

答案:构造块 构造块 静态块 构造块

回答出以下代码的执行结果(字符串常量池)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String s1 = "HelloWorld";
String s2 = new String("HelloWorld");
String s3 = "Hello";
String s4 = "World";
String s5 = "Hello" + "World";
String s6 = s3 + s4;

System.out.println(s1 == s2);

System.out.println(s1 == s5);

System.out.println(s1 == s6);

System.out.println(s1 == s6.intern());

System.out.println(s2 == s2.intern());

解析

首先要理解以下这句代码创建了几个对象

1
String s = new String("abc");

答案是创建了2个对象
存放在堆区里的new出来的String对象和常量池中的字符串常量”abc”

回到这个例子

1
1行: String s1 = "HelloWorld";

JVM在常量池中寻找有没有字符串常量”HelloWorld”,发现没有,则创建了字符串常量”HelloWorld”,并返回了引用给s1

1
2行: String s2 = new String("HelloWorld");

JVM在常量池中寻找到字符串常量”HelloWorld”已经存在了,则不创建该字符串常量.
然后因为new String(),则在堆区中创建了一个String对象将字符串常量”HelloWorld”拷贝到该对象中,并返回堆区中的String对象的引用给s2

1
2
3行: String s3 = "Hello";
4行: String s4 = "World";

JVM在常量池中没有找到字符串常量”Hello”和”World””,则在常量池中创建了字符串常量”Hello”和”World”

1
5行: String s5 = "Hello" + "World";

JVM在常量池中找到存在字符串常量”Hello”和”World”,将其用”+”进行拼接成”HelloWorld”后
发现常量池中已经存在了字符串常量”HelloWorld”.则直接返回”HelloWorld”的引用给s5

1
6行: String s6 = s3 + s4;

首先s3和s4都是定义的变量,这个变量是指严格意义的变量,除了字面上的字符,所有至少需要处理一次才能成为直接字面字符的值Java都一律视为变量.
由于Java并不知道s3和s4到底是什么,从定义上看,只知道这两个变量时String类型的,到了运行时才会去确定变量的具体值.
所以Java会在运行的时候去处理”+”号两边的变量
对于字符串的运算,如果是变量,那么Java会先new出一个StringBuilder对象,然后调用append()方法将”+”两边的字符串拼接起来
最后通过toString()方法返回一个在堆区新的String对象,并将该对象引用返回给s6

最后,使用JavaAPI文档对intern()方法解释

1
2
3
4
5
6
7
8
9
public String intern()
返回字符串对象的规范化表示形式。
一个初始时为空的字符串池,它由类 String 私有地维护.
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。
否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。
它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t)true 时,s.intern() == t.intern() 才为 true
所有字面值字符串和字符串赋值常量表达式都是内部的。
返回:
一个字符串,内容与此字符串相同,但它保证来自字符串池中。

所以s2.intern() 和s6.intern() 返回的都是常量池中的字符串常量”HelloWorld”

综上所属
s1 == s2; s1是指向常量池中的”HelloWorld”,而s2是指向堆区中的字符串对象,所以false.
s1 == s5; s1是指向常量池中的”HelloWorld”,s5是两个字符串常量拼接后指向的的同一个”HelloWorld”,所以是true.
s1 == s6; s1是指向常量池中的”HelloWorld”,而s6是指向堆区中的String对象,所以是false.
s1 == s6.intern(); s1是指向常量池中的”HelloWorld”,s6.intern()是指向堆区中创建的String对象,随后通过调用intern()方法返回的在常量池中的同一个”HelloWorld”,所以是true.
s2 == s2.intern(); s2是指向堆区中的String对象,而s2.intern()是指向堆区中的String对象,随后通过调用intern()方法返回的在常量池中的字符串常量”HelloWorld”,所以是false

答案:false true false true false


观察以下代码,回答运行结果(自动装箱拆箱,数值缓存)

1
2
3
4
5
6
7
8
9
public class App {

public static void main(String[] args) {
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;

System.out.println(f1 == f2);
System.out.println(f3 == f4);
}
}

解析

首先f1,f2,f3,f4这四个变量都是Integer对象引用,所以==运算比较的是引用而不是值.
其次,这个问题肯定与装箱拆箱有关系,那什么是装箱拆箱呢?
Java是一个面向对象语言,为了编程的方便还是引入了基本数据类型,但是为了能让这些基本数据类型也可以当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(Wrapper Class).从JDK 5开始就引入了自动装箱/拆箱机制,可以让二者互相转换.

  • 原始类型: boolean,char,byte,short,int,long,float,double
  • 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

看以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AutoUnboxingTest {

public static void main(String[] args) {
Integer a = new Integer(4);
// 将4自动装箱成Integer类型
Integer b = 4;
int c = 4;
// false 两个引用没有引用同一对象
System.out.println(a == b);
// true a自动拆箱成int类型再和c比较
System.out.println(a == c);
}
}

回到开始的问题,现在了解了装箱,当我们给一个Integer对象赋予一个int值的时候,就会自动的调用Integer类的静态方法valueOf(int i).可以看一下Integer类里面valueOf(int i)的JDK源码

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

这里调用了IntegerCache类的方法,IntegerCache是Integer类的内部类,同样也看一下源码

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 {
// high value may be configured by property
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);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

从这里可以看出,如果整型字面量的值在-128到127之间,不会去创建新的Integer对象,而是直接引用常量池中的Integer对象.
所以综上所述f1和f2是指向的同一个Integer对象,而f3和f4指向的是重新开辟空间来存储的Integer对象.

答案: true false

为什么FileInputStream的read()方法返回值是要设计成int而不是byte

解析:

首先我们从计算机系统是如何存储数值的说起,计算机存储数值都是使用的补码来存储.这时就牵扯到了原码,反码和补码的知识点.

计算机为什么要用补码才存储数值呢?

  1. 符号位和有效值一起处理,可以用加法来代替减法运算
  2. 因为正数0和负数0的原码不同,避免了0的编码不一样

原码,反码和补码是怎么计算呢?

这时引入两个名词:无符号数 有符号数
无符号数:所有的二进制位都用来表示数值的绝对值
有符号数:最高位作为符号位,”0”表示正数,”1”表示负数;其余二进制位用来表示数值的绝对值
对于无符号数来说,没有原码,反码和补码之分,因为三者都相同
对于有符号数来说,正数的原码,反码和补码三者相同的;而对于负数来说就有计算了,例如:

1
2
3
4
5
6
7
8
9
1: -12
原码:10001100
反码:11110011(在原码的基础上,符号位不变,其余位置取反)
补码:11110100(在反码的基础上,符号位不变,加+1)

2: +12
原码:00001100
反码:00001100
补码:00001100

回到这个问题.
接下来,read()方法,它一次读取一个字节,返回下一个字节的数据,当返回值是-1的时候,就停止读取.
那首先我们来计算出-1的补码是多少

1
2
3
4
数字: -1
原码: 10000001
反码: 11111110
补码: 11111111

其次int是4个字节,而byte是1个字节
如果是byte来读的话,根据1个字节=8个二进制位
现在我们假设有一个文件的内容用二进制表示为:

1
01100011 00111001 11111111 00011100

当它读到第二个字节的时候,就返回了下一个字节的数据11111111,这个值等于-1.就会直接停止读取,导致文件没读完.
而现在换成int,4个字节=32个二进制位
当它读到第二个字节的时候,返回下一个字节的数据,只有8个二进制位,则缺位补0,就会变成

1
00000000 00000000 00000000 11111111

这个值等于255,则不是-1,可以继续往下读.读完的时候返回结束标记-1,和int类型的-1进行判断,true,停止读.
当写文件的时候,会把补位的0全都给抹掉.

以上就是为什么read()方法的返回值类型是int而不是byte的原因了,如有错误,望指正.

您的支持将让服务器运行更长久