欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java常见面试问题-part2

程序员文章站 2024-03-23 22:19:16
...

如有问题,敬请指正!

11. String 转换成 Integer 的方式及原理

  1. 使用 Integer 类的 Integer.parseInt() 方法

    public static void main(String[] args) {
            String s = "17";
            Integer i = Integer.parseInt(s);
            boolean res = i.equals(13);
            System.out.println(i);	// 输出 17
            System.out.println(res);	// 输出 false
        	Integer num = Integer.parseInt(s, 8);
        	System.out.println(num);	// 输出 15 = 1 * 8^1 + 7 * 8^0
    }
    
  2. 上述方法默认使用十进制解析,也可以传入参数指定将 s 按参数进制解析

    1. 11 - 22 行进行参数检验,包括字符串和进制是否符合要求。
      1. 字符串不为空,否则抛出异常
      2. 进制在 [2,36] 之间,否则抛出异常
      3. 开始处理字符串,23 行之后
        1. 取字符串第一位字符,根据字符的 ASCII 码与 ‘0’ 比较,判断是不是 ‘+’ 或 ‘-’
        2. 确定正负数后,逐步获取每位字符的 int 值;
        3. 通过 *= 和 -= 对各结果进行拼接
    public static int parseInt(String s) throws NumberFormatException {
            return parseInt(s,10);
    }
    // 指定基数 radix
    public static int parseInt(String s, int radix) throws NumberFormatException {
        /*
         * WARNING: This method may be invoked early during VM initialization
         * before IntegerCache is initialized. Care must be taken to not use
         * the valueOf method.
         */
        if (s == null) {
            throw new NumberFormatException("null");
        }
        if (radix < Character.MIN_RADIX) {
            throw new NumberFormatException("radix " + radix +
                                            " less than Character.MIN_RADIX");
        }
        if (radix > Character.MAX_RADIX) {
            throw new NumberFormatException("radix " + radix +
                                            " greater than Character.MAX_RADIX");
        }
    
        int result = 0;
        boolean negative = false;
        int i = 0, len = s.length();
        int limit = -Integer.MAX_VALUE;
        int multmin;
        int digit;
    	// 主体思路是首先判断第一位是否是符号,然后逐位转换
        if (len > 0) {
            char firstChar = s.charAt(0);
            // ‘-’ 和 ‘+’ 的ASCII码分别为45和43,都小于字符‘0’的ASCII码48
            if (firstChar < '0') { // Possible leading "+" or "-"
                if (firstChar == '-') {
                    negative = true;	// 负数
                    limit = Integer.MIN_VALUE;
                } else if (firstChar != '+')
                    throw NumberFormatException.forInputString(s);
    
                if (len == 1) // Cannot have lone "+" or "-"
                    throw NumberFormatException.forInputString(s);
                i++;
            }
            multmin = limit / radix;
            while (i < len) {
                // Accumulating negatively avoids surprises near MAX_VALUE
                // 这个方法比较关键,主要作用是根据字符的ASCII码把字符转换成int类型,如字符‘2’,则返回int类型的2,具体分析看下面;
                digit = Character.digit(s.charAt(i++),radix);
                if (digit < 0) {
                    throw NumberFormatException.forInputString(s);
                }
                if (result < multmin) {
                    throw NumberFormatException.forInputString(s);
                }
                // 这边就是对返回int作一系列的拼接运算,得到个、百、千位的结果
                result *= radix;
                if (result < limit + digit) {
                    throw NumberFormatException.forInputString(s);
                }
                result -= digit;
            }
        } else {
            throw NumberFormatException.forInputString(s);
        }
        return negative ? result : -result;
    }
    

12. 静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?

1. 是否可以被重写?

  1. 我们先看代码

    public class Test2 {
        public static void main(String[] args) {
            Son1 s = new Son1();
            s.printFatherStatic();  // 输出:子类继承父类静态属性:我是爸爸的静态属性
            s.printStaticMethod();  // 输出:我是父类的静态方法
            System.out.println(s.fatherStatic);   // 输出:我是爸爸的静态属性
        }
    }
    
    class Father1 {
        public static String fatherStatic = "我是爸爸的静态属性";
        public static void printStaticMethod() {
            System.out.println("我是父类的静态方法");
        }
    }
    
    class Son1 extends Father1 {
        public void printFatherStatic() {
            System.out.println("子类继承父类静态属性:" + fatherStatic);
        }
    }
    
  2. 通过上面的代码,我们可以看出,子类可以直接使用定义在父类内部的静态属性,也可以调用父类的静态方法,所以说明,子类可以继承父类的静态属性和静态方法

  3. 原因:

    1. 因为静态方法和静态属性在程序开始运行开始后就已经分配了内存,所有引用到该方法和属性的对象所指向的都是同一块内存区域中的数据,也就是该静态方法、静态属性;

2.是否可以被重写?

  1. 在上述代码的基础上,加上几行,报错!

Java常见面试问题-part2

  1. 如果不加注解,直接写方法,相当于是在子类重写定义了一个方法,这个方法并不是对父类静态方法的重写,仅仅是定义了一个和父类静态方法名字一样的子类方法。

  2. 原理同上: 子类中如果定义了相同名称的静态方法,并不会重写,而应该是在内存中又分配了一块给子类的静态方法,没有重写这一说 。

13. 成员内部类、静态内部类、局部内部类和匿名内部类的理解,以及项目中的应用

​ 内部类只是 Java 编译器的概念,对于 JVM 而言,每个内部类最终都会被编译成一个独立的类,生成独立的字节码文件。内部类可以访问外部类的私有变量方法内部类(局部内部类)是在一个方法内定义和使用的;匿名内部类使用范围很小,他们都不能在外部使用。 成员内部类和静态内部类可以被外部使用,不过他们都可以被声明成 private,即不能被外部使用了。

1. 静态内部类

  1. 内部类访问了外部类的一个私有变量 shared,其实私有变量是不能被类外部访问的,Java 的解决方法是:自动为 Outer1 生成一个非私有的方法 access$0,他返回这个私有静态变量 shared。
  2. 使用场景:如果他与外部类关系密切,且不依赖于外部类实例,则可以考虑定义为静态内部类。
public class Outer1 {
    private static int shared = 100;
    // 静态内部类
    public static class StaticInner {
        // 可以访问外部类的私有静态变量
        public void innerMethod() {
            System.out.println("inner = " + shared);
        }
    }
    // 在类内部,可以直接使用静态内部类
    public void test() {
        StaticInner si = new StaticInner();
        si.innerMethod();
    }
}
class Outside {
    // public 静态内部类可以被外部使用,不过要用 “外部类.静态内部类” 的方式使用
    public static void main(String[] args) {
        Outer1.StaticInner osi = new Outer1.StaticInner();
        osi.innerMethod();
    }
}

2.成员内部类

  1. 成员内部类对象总是与一个外部类对象相连的。 不能直接使用 new Outer2.Inner() 的方法创建对象,应该先创建一个外部类对象
  2. 成员内部类内部不能定义静态的变量和方法,因为成员内部类与外部是息息相关的,不应该单独使用;
  3. 使用场景:如果内部类与外部类关系密切,需要访问外部类的实例变量和方法,则可以考虑定义为成员内部类。外部类的一些方法的返回值可能是某个接口,为了返回这个接口,外部类方法可能使用内部类实现这个接口,这个内部类可以被设计为 private,对外完全隐蔽。
// 创建一个成员内部类对象
Outer2 outer2 = new Outer2();
Outer2.Inner inner = outer2.new Inner();
inner.innerMethod();
public class Outer2 {
    private int a = 100;
    public class Inner {
        public void innerMethod() {
            // 成员内部类可以访问外部类的实例变量和方法
            System.out.println("outer a = " + a);
            // 这种写法一般在重名时使用,否则 Outer.this 是多余的
            Outer2.this.action();
        }
    }
    public void action() {
        System.out.println("action");
    }
    public void test() {
        Inner inner = new Inner();
        inner.innerMethod();
    }
}

3.方法内部类(局部内部类)

  1. 只能在定义的方法内使用;
  2. 方法内部类可以访问方法的参数和方法中的局部变量,不过,在 Java 8 之前,这些局部变量都必须被声明成 final, Java 8 不再要求,但变量不能被重新赋值,否则会有编译错误。实际上,方法内部类操作的并不是外部的变量,而是他自己的实例变量,只是这些变量的值和外部一样,对这些变量赋值,并不会改变外部的值,为避免混淆,干脆规定必须声明为 final。
  3. 如果的确需要修改外部的变量,那么可以将变量改为只包含该变量的数组,修改数组中的值。 例如 str 是一个只含一个元素的数组,方法内部类不能修改 str 本身,但是可以修改 str 的数组元素。

4.匿名内部类

  1. 没有单独的类定义,在创建对象的时候定义类。

    new 父类(参数列表) {
        // 匿名内部类实现部分
    }
    new 父接口() {
        // 匿名内部类实现部分
    }
    
  2. 只能使用一次,没有名字,没有构造方法,但可以根据参数列表调用父类的构造方法

  3. 匿名内部类能做到的,方法内部类都能做。

15. 讲一下常见编码方式?

1.非 Unicode 编码

包括 ASCII(美)、ISO 8859-1、Windows-1252(西欧)、(后中国)GB2312、GBK、GB18030 和 Big5

  1. ASCII
    1. 128个字符的二进制表示方法,8位表示,最高位为0,剩下7位表示字符
    2. 数字 0 - 9,用 ‘48’ - ‘57’ 表示
    3. 大写字母 A - Z 用 ‘65’ - ‘90’ 表示
    4. 小写字母 a - z 用 ‘97’ = ‘122’ 表示(在大写的基础上加32)
    5. 不够表示其他国家的字符
  2. GBK
    1. 建立在 GB2312 的基础上,向下兼容。GBK 增加了 14000 多个汉字,共计约 21000个汉字,包括繁体字
    2. 使用固定的两个字节表示,高位范围是 0X81~0XFE,地位字节范围是 0X40~0X7E 和 0X80~0XFE

2.Unicode 编码

Unicode 给世界上所有字符都分配了一个唯一的数字编号。 编号怎么对应到二进制表示,主要由 UTF-32、UTF-16 和 UTF-8 三种方案。

  1. UTF-32
    1. 每个字符都用4个字节表示,不兼容 ASCII
  2. UTF-16
    1. 使用变长字节表示,大多两个字节,不兼容 ASCII
  3. UTF-8
    1. 使用变长字节表示,使用的字节数 1~4个,兼容 ASCII

16.Java 当中的四种引用

1.强引用

  1. 通过类似 Object obj = new Object(); 创建的引用,称之为“强引用”。
  2. 特点:
    1. 其指向的对象无论如何都不会被 JVM 垃圾回收器回收,即使面临 OutofMemoryError 的风险;

2.软引用

  1. 描述一些还有用但非必须的对象。 对于软引用关联着的对象,如果新创建对象时,系统即将发生内存溢出,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,就会抛出内存溢出异常。

3.弱引用

  1. 描述非必须的对象,强度低于软引用。
  2. 被引用的对象只能生存到下一次垃圾回收之前。当垃圾回收器工作时,不关当前内存是否足够,都会回收掉被弱引用管理的对象。

4.虚引用

  1. 又称 幽灵引用幻影引用
  2. 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来获得一个对象实例。
  3. 设置的目的:能在这个对象被回收时收到一个系统的通知。

17.深拷贝和浅拷贝的区别是什么?

1.浅拷贝

被拷贝对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。

直接调用super.clone实现的clone方法全部都是浅拷贝

8中基本类型和不可变对象属于直接拷贝值的类型。

Java常见面试问题-part2

  1. 对引用类型的改变会相互影响,但是基本类型就不存在影响。
  2. 如何实现浅拷贝:
class Subject {
    private String name;
    
    public Subject(String s) {
        name = s;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String s) {
        name = s;
    }
}
class Student implements Cloneable {
    // 对象引用
    private Subject subj;
    private String name;
    
    public Student(String s, String sub) {
        name = s;
        subj = new Subject(sub);
    }
    
    public Subject getSubj() {
        return subj;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String s) {
        name = s;
    }
    /**
     *  重写clone()方法
     * @return
     */
    public Object clone() {
        //浅拷贝
        try {
            // 直接调用父类的clone()方法
            return super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
}


public class ShallowCopy {
    public static void main(String[] args) {
        // 原始对象
        Student stud = new Student("勒布朗", "詹姆斯");
        System.out.println("原始对象: " + stud.getName() + " - " + stud.getSubj().getName());

        // 拷贝对象
        Student clonedStud = (Student) stud.clone();
        System.out.println("拷贝对象: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());

        // 原始对象和拷贝对象是否一样:
        System.out.println("原始对象和拷贝对象是否一样: " + (stud == clonedStud));
        // 原始对象和拷贝对象的name属性是否一样
        System.out.println("原始对象和拷贝对象的name属性是否一样: " + (stud.getName() == clonedStud.getName()));
        // 原始对象和拷贝对象的subj属性是否一样
        System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (stud.getSubj() == clonedStud.getSubj()));

        stud.setName("布莱恩特");
        stud.getSubj().setName("科比");
        System.out.println("更新后的原始对象: " + stud.getName() + " - " + stud.getSubj().getName());
        System.out.println("更新原始对象后的克隆对象: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());
    }
}


输出

原始对象: 勒布朗 - 詹姆斯
拷贝对象: 勒布朗 - 詹姆斯
原始对象和拷贝对象是否一样: false
原始对象和拷贝对象的name属性是否一样: true
原始对象和拷贝对象的subj属性是否一样: true
更新后的原始对象: 布莱恩特 - 科比
更新原始对象后的克隆对象: 勒布朗 - 科比


  1. 说明,对原始对象 stud 的 name 属性的改变没有影响拷贝对象;对原始对象 stud 的引用属性 subj 的改变影响了拷贝对象。

2.深拷贝

被拷贝对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。 速度较慢且开销大。

Java常见面试问题-part2

  1. 两边变动相互不影响
  2. 相比于浅拷贝的改变在于,重写 clone 方法:
   /** 
    * 重写clone()方法 
    */ 
   public Object clone() { 
      // 深拷贝,创建拷贝类的一个新对象,这样就和原始对象相互独立
      Student s = new Student(name, subj.getName()); 
      return s; 
   }


  1. 需要先创建拷贝类的一个对象,再返回这个对象。

18.什么是编译期常量,有什么风险?

常量用 static final 修饰

  1. 编译期常量:程序在编译时就能确定这个常量的具体值;
  2. 非编译期常量:程序在运行时才能确定常量的值,也称运行时常量。

定义上来讲,声明为 final 类型的基本类型或者 String 类型并直接赋值(非运算)的变量就是编译期常量

// 编译期常量
final int i = 4;
final String str = "abcd";
// 非编译期常量
final int i = rand.nextInt();


注意:由于在编译期常量在编译时就确定了值,使用编译期常量的地方在编译时会替换成对应的值即字面量。

我们一般将编译期常量声明为public static final类型(静态常量),在这种情况下,引用编译期常量不会导致类的初始化 。 本来引用 static 变量会引起类加载器加载常量所在类,并进行初始化,但由于是编译期常量,编译器在编译引用这个常量的类时,会直接将常量替换为对应值,也就无需再去加载常量所在的类了。

编译期常量不依赖类,不会引起类的初始化;而运行时常量依赖类,会引起类的初始化

  1. 风险

如果我们项目很大大,项目整个编译一次特别耗费时间,那么我们有可能会只编译代码修改的部分。而一旦我们修改了编译期常量A,但又未重新编译所有引用编译期常量A的部分(即.java文件),那么就会导致未重新编译的那部分代码继续使用编译期常量A的旧值。

所以,在更新依赖文件时,要确保重新编译自己的文件,避免依旧使用原有的编译期常量旧值,产生错误。

19.你对 String 对象的 intern() 熟悉吗?

  1. 首先intern() 是 String 类的一个本地方法。可以减少内存中相同字符串的数量,节省一些空间。

    public native String intern();
    
    
    
  2. 根据该方法的注释,我们可以得到如下结论:

    1. 方法返回字符串对象的规范化表示形式;
    2. String 类最初有一个空的私有的字符串池;
    3. intern() 方法调用的时候,如果常量池中已经包含调用intern() 方法的字符串时,返回的是常量池中的字符串;否则,将这个调用者字符串加入常量池,并返回这个字符串的引用。也就是说,对于两个字符串 s 和 t,s.intern() == t.intern() 当且仅当 s.equals(t) == true
  3. new String("xxx") 都是在堆上创建字符串对象。当调用intern() 方法时,编译器会将字符串添加到常量池中,并返回指向该常量的一个引用。

    1. new String("xxx"); 是在堆上创建字符串对象;Java常见面试问题-part2

    2. 若调用 Str1.intern() ,编译器会将字符串 “hello” 添加到字符串常量池中,并返回指向该常量的引用。如果此后再去调用 Str2.intern() ,因为常量池中已经存在 “hello”,则直接返回常量池中 ''hello" 的引用。(JDK 1.6)Java常见面试问题-part2

  4. 通过字面量赋值创建字符串时,会先在常量池中查找是否存在相同的字符串,若存在,则将引用直接指向常量池中的字符串;如果不存在,则在常量池中生成一个字符串,再将该字符串的引用指向接收者。

    String Str1 = "hello";
    String Str2 = "hello";
    String Str3 = "world";
    
    
    

    根据上面三行代码,有如下图示:

    Java常见面试问题-part2

  5. **对于字符串的 “+” 操作,在编译阶段会直接合并成一个字符串,相当于创建了一个字面量字符串。**如:String S1 = "Hello" + " World"; 在编译阶段会被合并成为String S1 = “Hello World”; 剩下的操作就和通过字面量创建字符串一样了。

  6. 对于 final 字段修饰的,编译期直接进行了常量替换;

  7. 常量字符串和变量拼接时,会调用StringBuilder.append(); 在堆上创建新的对象;例如:String S1 = str1 + "world";

  8. JDK 1.7 以后,intern() 方法还是会去检查常量池中是否存在调用者字符串,如果存在就直接返回引用;如果不存在与 JDK 1.6 不同的是,不再创建一个字符串的拷贝再加入常量池,而是在常量池中生成一个对应字符串的引用。 也就是说,原来在常量池中找不到就复制一个放进去,1.7 以后就是将堆上的引用复制到常量池中。

  9. 举栗说明:

    public static void main(String[] args) {
        String str2 = new String("hello") + new String(" world");
        str2.intern();
        String str1 = "hello world";
        System.out.println(str2 == str1);	// true
    }
    // 执行 str2.intern(); 的时候常量池中还没有 "hello world" 字符串,所以将 "hello world" 的在堆中的引用放入常量池;当执行 String str1 = "hello world";时,常量池中已经有一个地址了,所以直接返回那个地址,也就是 str2 指向的地址。顾两个地址相同为 true
    
    
    
    public static void main(String[] args) {
        String str2 = new String("hello") + new String(" world");
        String str1 = "hello world";
        str2.intern();
        System.out.println(str2 == str1);	// false
    }
    // 第2行创建的对象放在堆中,即 str2 指向堆中的地址
    // 第3行,此时常量池中没有 "hello world",所以在常量池中创建一个 "hello world"
    // 第4行,此时返回一个常量池中的引用,没人接收,所以 str2 还是指向堆中的地址
    // 如果改为如下代码,则输出 true
    	str2 = str2.intern(); // 相当于用 str2 来接收常量池中的 "hello world" 引用。
    
    
    

20.a = a + ba += b 有什么区别?

1. a = a + b

  1. 我们先看一段代码,关于 a = a + b; 为方便看编译器警告,直接截图:

Java常见面试问题-part2

  1. 原因:
  • a 是一个 byte 类型,而数字 4 是一个 int 类型。在 java 中进行运算时,会进行自动类型转换,将 a + 4 转换成 int 类型,但是变量 a 是 byte 类型,将 int 类型 a + 4 强制转换成 byte 会产生错误(低转高可以,高转低在强制类型转换的条件下可能出现溢出错误)。
  • 如果尝试进行强制类型转换,在 byte 取值范围内可行,但是当超出范围,会出现溢出错误或者精度下降,如下

Java常见面试问题-part2

2.a += b

  1. 直接先看代码
public class Test3 {
    public static void main(String[] args) {
        byte a = 4;
        a += 4;
        System.out.println(a);  // 输出 8,转换正常
    }
}


  1. += 是 java 中的一个运算符,在运算过程中,首先会进行自动类型转换,所以在编译时没有报错。
  2. 但是!!!!!!!!!!仅仅只是在编译时不报错,超出还是会溢出或精度下降,如果出现下列情况
public class Test3 {
    public static void main(String[] args) {
        byte a = 4;
        a += 124;
        System.out.println(a);  // 输出 -128,byte 溢出,得到不是我们想要的结果
    }
}


3.区别

  1. 在两个变量的数据类型一致时,a = a + ba += b 没有区别;
  2. 当两个变量的数据类型不同时,需要考虑数据类型的自动转换问题;
  3. 低位转高位没问题,但是当出现需要高位转向低位时,都要考虑溢出或者精度下降的问题。
  4. byte -> short -> int -> long -> float -> double
数据类型 byte short int(默认) long float double(默认)
占用字节数 1 2 4 8 4 8

21.throw和throws的区别

1.throw

  1. 就是抛出异常,会触发 Java 的异常处理机制
  2. return 代表正常退出,throw 代表异常退出;return 的返回值是确定的,就是上一级调用者,而 throw 后执行哪行代码经常是不确定的,由异常处理机制动态确定

2.throws

  1. 用于声明一个方法可以抛出异常,可以有多个,用逗号隔开
  2. 且没有对这个异常进行处理,至少没有处理完,调用者必须对其进行处理。