本笔记为个人学习思考解惑记录,多方面引用互联网资料,争取每日更新。
若有纰漏,欢迎指正。
2018年07月07日
1. 观察一下代码,说出执行结果(JVM类加载)
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会确保这个类已经被加载、连接(验证、准备和解析)及初始化。
- 类的加载是指把类的.class文件中的数据读取到内存中,通常是创建一个字节数组读入.class文件,然后产生所加载类的Class对象。加载完成后,Class对象还不完整,此时的类还不可用。
- 进入连接阶段,该阶段又分为3阶段
- 验证节点: 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件。验证内容涵盖了类数据信息的格式验证、语义验证、操作验证等。
- 文件格式验证:验证是否符合.class文件规范
- 语义验证(元数据验证):检查一个被标记为final的类时候包含子类;检查final方法时候被重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但返回值不同)……
- 操作验证(字节码验证):保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(比如不应该出现在操作栈中放置了一个int类型的数据,使用时却按long类型来加载如本地变量表中);保证跳转指令不会跳转到方法体以外的字节码指令上……
- 准备阶段: 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于未产生对象,实例变量不在此操作范围内),被final修饰的静态变量,会直接赋值原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值。
- 解析阶段: 将常量池中的符号引用转为直接引用,得到类或者字段、方法在内存中的地址或者相对偏移量,以便指针直接调用该方法。
- 验证节点: 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件。验证内容涵盖了类数据信息的格式验证、语义验证、操作验证等。
什么是符号引用?
就是以一组字面量来描述所引用的目标。是相对于直接引用的。符号引用包括三类常量,分别是类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
初始化: 将类中所有被static关键字修饰的代码统一执行一次,如果执行的是静态变量,那么就会使用开发者赋予的值来覆盖先前准备阶段的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作;如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;如果类中存在初始化语句,就依次执行这些初始化语句。
- 初始化阶段是执行类的构造器
2. 回答出以下代码的执行结果(字符串常量池)
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());
解析
首先要理解以下这句代码创建了几个对象
String s = new String("abc");
答案是创建了2个对象
存放在堆区里的new出来的String对象和常量池中的字符串常量”abc”
回到这个例子
第1行: String s1 = "HelloWorld";
JVM在常量池中寻找有没有字符串常量”HelloWorld”,发现没有,则创建了字符串常量”HelloWorld”,并返回了引用给s1
第2行: String s2 = new String("HelloWorld");
JVM在常量池中寻找到字符串常量”HelloWorld”已经存在了,则不创建该字符串常量.
然后因为new String(),则在堆区中创建了一个String对象将字符串常量”HelloWorld”拷贝到该对象中,并返回堆区中的String对象的引用给s2
第3行: String s3 = "Hello";
第4行: String s4 = "World";
JVM在常量池中没有找到字符串常量”Hello”和”World”“,则在常量池中创建了字符串常量”Hello”和”World”
第5行: String s5 = "Hello" + "World";
JVM在常量池中找到存在字符串常量”Hello”和”World”,将其用”+”进行拼接成”HelloWorld”后
发现常量池中已经存在了字符串常量”HelloWorld”.则直接返回”HelloWorld”的引用给s5
第6行: String s6 = s3 + s4;
首先s3和s4都是定义的变量,这个变量是指严格意义的变量,除了字面上的字符,所有至少需要处理一次才能成为直接字面字符的值Java都一律视为变量.
由于Java并不知道s3和s4到底是什么,从定义上看,只知道这两个变量时String类型的,到了运行时才会去确定变量的具体值.
所以Java会在运行的时候去处理”+”号两边的变量
对于字符串的运算,如果是变量,那么Java会先new出一个StringBuilder对象,然后调用append()方法将”+”两边的字符串拼接起来
最后通过toString()方法返回一个在堆区新的String对象,并将该对象引用返回给s6
最后,使用JavaAPI文档对intern()方法解释
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
2018年07月08日
3.观察以下代码,回答运行结果(自动装箱拆箱,数值缓存)
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
看以下代码
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源码
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类的内部类,同样也看一下源码
/**
* 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
2018年07月10日
4.为什么FileInputStream的read()方法返回值是要设计成int而不是byte
解析:
首先我们从计算机系统是如何存储数值的说起,计算机存储数值都是使用的补码来存储.这时就牵扯到了原码,反码和补码的知识点.
计算机为什么要用补码才存储数值呢?
- 符号位和有效值一起处理,可以用加法来代替减法运算
- 因为正数0和负数0的原码不同,避免了0的编码不一样
原码,反码和补码是怎么计算呢?
这时引入两个名词:无符号数 有符号数
无符号数:所有的二进制位都用来表示数值的绝对值
有符号数:最高位作为符号位,”0”表示正数,”1”表示负数;其余二进制位用来表示数值的绝对值
对于无符号数来说,没有原码,反码和补码之分,因为三者都相同
对于有符号数来说,正数的原码,反码和补码三者相同的;而对于负数来说就有计算了,例如:
例1: -12
原码:10001100
反码:11110011(在原码的基础上,符号位不变,其余位置取反)
补码:11110100(在反码的基础上,符号位不变,加+1)
例2: +12
原码:00001100
反码:00001100
补码:00001100
回到这个问题.
接下来,read()方法,它一次读取一个字节,返回下一个字节的数据,当返回值是-1的时候,就停止读取.
那首先我们来计算出-1的补码是多少
数字: -1
原码: 10000001
反码: 11111110
补码: 11111111
其次int是4个字节,而byte是1个字节
如果是byte来读的话,根据1个字节=8个二进制位
现在我们假设有一个文件的内容用二进制表示为:
01100011 00111001 11111111 00011100
当它读到第二个字节的时候,就返回了下一个字节的数据11111111,这个值等于-1.就会直接停止读取,导致文件没读完.
而现在换成int,4个字节=32个二进制位
当它读到第二个字节的时候,返回下一个字节的数据,只有8个二进制位,则缺位补0,就会变成
00000000 00000000 00000000 11111111
这个值等于255,则不是-1,可以继续往下读.读完的时候返回结束标记-1,和int类型的-1进行判断,true,停止读.
当写文件的时候,会把补位的0全都给抹掉.
以上就是为什么read()方法的返回值类型是int而不是byte的原因了,如有错误,望指正.