Java问题日常随记

本笔记为个人学习思考解惑记录,多方面引用互联网资料,争取每日更新。
若有纰漏,欢迎指正。


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会确保这个类已经被加载、连接(验证、准备和解析)及初始化。

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

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

  1. 初始化: 将类中所有被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

解析:

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

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

  1. 符号位和有效值一起处理,可以用加法来代替减法运算
  2. 因为正数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的原因了,如有错误,望指正.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值