0%

面试题-Jdk基础

本章属于持续学习、长期更修。

声明一个Double变量赋值 0.001会编译错误吗 ?

  • 不会,double 数据类型是双精度、64 位、标准的浮点数。

Java是纯粹的面向对象语言吗?

  • Java不是存粹的面向对象语言,因为它包含了原生数据类型,比如int 、double。

Java面向对象的特征?

  • 继承、封装、多态、抽象。

String、StringBuffer、StringBuilder的区别

  • 可变性
    • String 是不可变的,StringBuffer\StringBuilder是可变的。
    • String 类中使用 final 关键字字符数组保存字符串,private final char value[],所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
  • 线程安全方面
    • String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父类,但是StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
  • 性能
    • 操作少量的数据 = String
    • 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
    • 多线程操作字符串缓冲区下操作大量数据 = StringBuffer

为什么 String 类型要用 final 修饰?

使用 final 修饰的第一个好处是安全;第二个好处是高效,以 JVM 中的字符串常量池来举例,如下两个变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) {
String s1 = "java";
String s2 = "java";

// 以上两个局部变量都存在了常量池中
System.out.println(s1 == s2); // true


// new出来的对象不会放到常量池中,内存地址是不同的
String s3 = new String();
String s4 = new String();

/**
* 字符串的比较不可以使用双等号,这样会比较内存地址
* 字符串比较应当用equals,可见String重写了equals
*/
System.out.println(s3 == s4); // false
System.out.println(s3.equals(s4)); // true
}
}
  • 只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们缓存字符串,提高程序的运行效率,如下图所示:

  • 工作原理

  当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。

== 和 equals

  • == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)

  • equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

    • 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
    • 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
    • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
    • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

i++和++i有什么区别

  • i++ 是在程序执行完毕后进行自增,而 ++i 是在程序开始执行前进行自增。
    • i++ 的操作分三步
      • 栈中取出 i
      • i 自增 1
      • 将 i 存到栈
      • 三个阶段:内存到寄存器,寄存器自增,写回内存(这三个阶段中间都可以被中断分离开)
      • 所以 i++ 不是原子操作,上面的三个步骤中任何一个步骤同时操作,都可能导致 i 的值不正确自增。
    • ++i
      • 在多核的机器上,CPU 在读取内存 i 时也会可能发生同时读取到同一值,这就导致两次自增,实际只增加了一次。
      • i++ 和 ++i 都不是原子操作。
    • 原子性:指的是一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程打断。

Java**的四种引用,强弱软虚**

  • 强引用

    • 强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:
    1
    String str = new String("str");
  • 软引用

    • 软引用在程序内存不足时,会被回收,使用方式:
    1
    2
    3
    4
    // 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的, 
    // 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
    SoftReference<String> wrf = new SoftReference<String>(new String("str"));
    // 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
  • 弱引用

    • 弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:
    1
    2
    3
    WeakReference<String> wrf = new WeakReference<String>(str);
    // Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用
    // 不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作
  • 虚引用

    虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue ,使用。

    1
    2
    PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());
    // 对象销毁前的一些操作,比如说资源释放等。** Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效x

为什么要重写equals和hashcode方法

  • equals是Object的成员方法,默认不重写(override)情况下判断等价性。
    • 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
    • 类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class test {
public void test() {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
  • hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。

    • 集合中使用场景。
      • 将对象放入到集合中时,首先判断要放入对象的 hashCode 值与集合中的任意一个元素的 hashCode 值是否相等,如果不相等直接将该对象放入集合中。
      • 如果 hashCode 值相等,然后再通过 equals 方法判断要放入对象与集合中的任意一个对象是否相等,如果 equals 判断不相等,直接将该元素放入到集合中,否则不放入。
  • hashCode()与equals()的相关规定

    1. 如果两个对象相等,则hashcode一定也是相同的。
    2. 两个对象相等,对两个对象分别调用equals方法都返回true。
    3. 两个对象有相同的hashcode值,它们也不一定是相等的。
    4. 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
    5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

描述下final 与 finally、finalize的区别。

  • final ,是修饰符关键字。

    • 如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在new一个对象时初始化(即只能在声明变量或构造器或代码块内初始化),而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能覆盖(重写)。

      • 父类的private成员方法是不能被子类覆盖的,因为被private修饰的方法默认是final类型的。

      • final类

        • final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类。
      • final方法

        • 如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。
      • final变量(常量)

        • 用final修饰的成员变量表示常量,值一旦给定就无法改变!
        • final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
        • final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。类中的final数据成员就可以根据依赖对象而有所不同,并保持其恒定不变的特征。
      • final参数

        • 当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值。
      • 注意final 不是immutable的

  • finally在异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。

    • 在异常处理时提供 finally 块来执行任何清除操作。只有在与 finally 相对应的 try 语句块得到执行的情况下,finally 语句块才会执行。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
    • finally 语句块可能是要执行的。
      • 当try流程中出现程序中断情况是不会在执行finally语句的。也就是说一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。
  • finalize ,是方法名。

    • Java 允许使用 #finalize() 方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。

finally对应的try catch语句流程

1
2
3
4
5
6
7
8
9
10
11
   public static void main(String[] args)  {
try {
System.out.println("try block");
return ;
} finally {
System.out.println("finally block");
}
}
//运行结果
//try block
//finally block

从结果看finally语句会在return之前执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args)  {
System.out.println("return value of test(): " + test());
}

public static int test() {
int i = 1;
try {
System.out.println("try block");
i = 1 / 0; //抛一个异常进catch
return i;
}catch (Exception e){
System.out.println("exception block");
return 2;
}finally {
System.out.println("finally block");
}
}
//运行结果
//try block
//exception block
//finally block
//return value of test(): 2

从结果看finally 语句块在 catch 语句块中的 return 语句之前执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args)  {
System.out.println("return value of test(): " + test());
}

@SuppressWarnings("finally")
public static int test() {
int i = 1;
try {
i = 4;
return i;
} finally {
i++;
return i;
}
}
//运行结果
//return value of test(): 5

从结果看 finally 语句块中如果出现return那么该流程就结束了。其实是finally块中的return语句会覆盖try块中的return返回。

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
 public static void main(String[] args)  {
System.out.println("return value of test3(): " + test());
}

public static int test3() {
int b = 20;
try {
System.out.println("try block");
b += 80;
return b;
} catch (Exception e) {
System.out.println("catch block");
} finally {
System.out.println("finally block");
if (b > 25) {
b += 100;
System.out.println("b>25 and b = " + b);
}
}
return b;
}
// 运行结果
//try block
//finally block
//b>25 and b = 200
//return value of test(): 100

从结果看:weary:为什么不返回200!为什么finally里面的修改没有效果?因为finally语句中没有return语句覆盖返回值,那么原来的返回值可能因为finally里的修改而改变也可能不变。那什么情况下会改变呢?

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
 public static void main(String[] args)  {
System.out.println("return value of test3(): " + test());
}

public static int test3() {
int b = 20;
try {
System.out.println("try block");
b += 80;
b = b / 0; //抛异常
return b;
}
catch (Exception e) {
System.out.println("catch block");
}
finally {
System.out.println("finally block");
if (b > 25) {
b += 100;
System.out.println("b>25 and b = " + b);
}
}
return b;
}
//运行结果
//try block
//catch block
//finally block
//b>25 and b = 200
//return value of test(): 200

从结果看finally里面的修改启效果了,因为抛出了异常所以没有执行try代码块里面的return。

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
    public static void main(String[] args)  {
System.out.println("return value of test3(): " + test());
}
public static int test3() {
int b = 20;
try {
System.out.println("try block");
b += 80;
b = b / 0;
return b;
}
catch (Exception e) {
System.out.println("catch block");
return b += 10;
}
finally {
System.out.println("finally block");
if (b > 25) {
b += 100;
System.out.println("b>25 and b = " + b);
}
}
}
// 运行结果
// try block
// catch block
// finally block
// b>25 and b = 210
// return value of test(): 110
// 从结果来看,抛出异常后return 方法执行之前,也运行了finally代码块,但是并未影响catch代码块中的返回值。
  • finally块的语句在try或catch中的return语句执行之后返回之前执行,finally里的修改语句可能影响也可能不影响try或catch中 return已经确定的返回值,若finally里也有return语句则覆盖try或catch中的return语句直接返回。没有进入try代码块就不会执行finally代码块。

static,this,super 关键字总结

static 关键字

static 关键字主要有以下四种使用场景

  1. 修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名 类名.静态方法名()
  2. 静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
  3. 静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
  4. 静态导包(用来导入类中的静态资源,1.5之后的新特性): 格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
修饰成员变量和成员方法(常用)
  • 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。

  • 方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

  • HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。

  • 调用格式:

    • 类名.静态变量名
    • 类名.静态方法名()
  • 如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。

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
public class StaticBean {

String name;
//静态变量
static int age;

public StaticBean(String name) {
this.name = name;
}
//静态方法
static void SayHello() {
System.out.println("Hello i am java");
}
@Override
public String toString() {
return "StaticBean{"+
"name=" + name + ",age=" + age +
"}";
}
}
public class StaticDemo {

public static void main(String[] args) {
StaticBean staticBean = new StaticBean("1");
StaticBean staticBean2 = new StaticBean("2");
StaticBean staticBean3 = new StaticBean("3");
StaticBean staticBean4 = new StaticBean("4");
StaticBean.age = 33;
System.out.println(staticBean + " " + staticBean2 + " " + staticBean3 + " " + staticBean4);
//StaticBean{name=1,age=33} StaticBean{name=2,age=33} StaticBean{name=3,age=33} StaticBean{name=4,age=33}
StaticBean.SayHello();//Hello i am java
}

}
静态代码块
  • 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
1
2
3
static {    
语句体;
}
  • 一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
静态内部类
  • 静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:
  1. 它的创建是不需要依赖外围类的创建。
  2. 它不能使用任何外围类的非static成员变量和方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {

//声明为 private 避免调用默认构造方法创建对象
private Singleton() {
}

// 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
  • 当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。

  • 这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

静态导包
  • 格式为:import static

  • 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法

1
2
3
4
5
6
7
8
9
10
11
12
 //将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用
//如果只想导入单一某个静态方法,只需要将换成对应的方法名即可

import static java.lang.Math.*;//换成import static java.lang.Math.max;具有一样的效果

public class Demo {
public static void main(String[] args) {

int max = max(1,2);
System.out.println(max);
}
}
静态方法与非静态方法
  • 静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo {
int i;
public Foo(int i) {
this.i = i;
}

public static String method1() {
return "An example string that doesn't depend on i (an instance variable)";

}

public int method2() {
return this.i + 1; //Depends on i
}

}
  • 你可以像这样调用静态方法:Foo.method1()。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:Foo bar = new Foo(1);bar.method2();

总结:

  • 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
  • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
static{}静态代码块与{}非静态代码块(构造代码块)
  • 相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。

  • 不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。

  • 一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的.

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
public class Test {
public Test() {
System.out.print("默认构造方法!--");
}

//非静态代码块
{
System.out.print("非静态代码块!--");
}

//静态代码块
static {
System.out.print("静态代码块!--");
}

private static void test() {
System.out.print("静态方法中的内容! --");
{
System.out.print("静态方法中的代码块!--");
}

}

public static void main(String[] args) {
Test test = new Test();
Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!--
}
}
  • 上述代码输出:
1
静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!--
  • 当只执行 Test.test(); 时输出:
1
静态代码块!--静态方法中的内容! --静态方法中的代码块!--
  • 当只执行 Test test = new Test(); 时输出:
1
静态代码块!--非静态代码块!--默认构造方法!--
  • 非静态代码块与构造函数的区别是: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。

this 关键字

this关键字用于引用类的当前实例。 例如:

1
2
3
4
5
6
7
8
9
10
11
class Manager {
Employees[] employees;

void manageEmployees() {
int totalEmp = this.employees.length;
System.out.println("Total employees: " + totalEmp);
this.report();
}

void report() { }
}

在上面的示例中,this关键字用于两个地方:

  • this.employees.length:访问类Manager的当前实例的变量。
  • this.report():调用类Manager的当前实例的方法。

此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。

super 关键字

super关键字用于从子类访问父类的变量和方法。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Super {
protected int number;

protected showNumber() {
System.out.println("number = " + number);
}
}

public class Sub extends Super {
void bar() {
super.number = 10;
super.showNumber();
}
}

在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 showNumber() 方法。

使用 this 和 super 要注意的问题

  • 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。
  • this、super不能用在static方法中。

简单解释一下:

  • 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西

Java程序初始化的顺序是怎么样的

 在 Java 语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建象。

初始化一般遵循3个原则:

  • 静态对象(变量)优先于非静态对象(变量)初始化,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次;
  • 父类优先于子类进行初始化;
  • 按照成员变量的定义顺序进行初始化。 即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化;

加载顺序

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

实例

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
class Base {
// 1.父类静态代码块
static {
System.out.println("Base static block!");
}
// 3.父类非静态代码块
{
System.out.println("Base block");
}
// 4.父类构造器
public Base() {
System.out.println("Base constructor!");
}
}

public class Derived extends Base {
// 2.子类静态代码块
static{
System.out.println("Derived static block!");
}
// 5.子类非静态代码块
{
System.out.println("Derived block!");
}
// 6.子类构造器
public Derived() {
System.out.println("Derived constructor!");
}
public static void main(String[] args) {
new Derived();
}
}

结果是:

1
2
3
4
5
6
Base static block!
Derived static block!
Base block
Base constructor!
Derived block!
Derived constructor!

介绍下异常类型

  • Throwable
    • Error
    • Exception
      • RuntimeException
      • IOException
  • 超类Throwable ,有两个子类Error和Exception,分别表示错误和异常。
    • Error是程序无法处理的错误,比如OutOfMemoryError等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
    • Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。
      • 运行时异常(RuntimeException)和非运行时异常也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception),这两种异常有很大的区别。
      • RuntimeException(运行时异常),表示无法让程序恢复的异常,导致的原因通常是因为执行了错误的操作,建议终止逻辑,因此,编译器不检查这些异常。
      • CheckedException(受检查异常),是表示程序可以处理的异常,也即表示程序可以修复(由程序自己接受异常并且做出处理),所以称之为受检查异常。

Throw 和 throws 的区别

  • throw ,用于在程序中显式地抛出一个异常。

  • throws ,用于指出在该方法中没有处理的异常。每个方法必须显式指明哪些异常没有处理,以便该方法的调用者可以预防可能发生的异常。最后,多个异常用逗号分隔。

  • 位置不同

    • throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
  • 功能不同

    • throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
    • throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。

描述下异常处理方式

  • 使用System.out.println是高代价的,这这做会降低系统吞吐量。
  • 在生成环境中避免使用printStackTrace()方法,printStackTrace默认会把调用的堆栈打印到控制台上,在生产环境中访问控制台是不现实的。
  • 如果不能处理异常,就不要捕获该异常。
  • 如果要捕获异常,应在最近的地方捕获它。
  • 不要吃掉你的捕捉的异常信息,就是捕获了啥也不做,建议LOG记录。
  • 不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调用者为了正常的控制流而使用异常)。
  • 对可以恢复的情况使用受检异常,对编程错误使用运行时异常。
  • 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)。
  • 优先使用标准的异常。
  • 每个方法抛出的异常都要有文档。
  • 保持异常的原子性
  • 不要在 catch 中忽略掉捕获到的异常。

常见的几种异常

  • NullPointerException
  • IndexOutOfBoundsException
  • ClassCastException
  • ArrayStoreException
  • BufferOverflowException

如何正确的在一个循环中删除ArrayList中的元素。

  • 如果使用普通for循环直接删除会出现IndexOutOfBoundsException异常,非法索引。

    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
    ArrayList<String> list = new ArrayList<String>();
    list.add("1");
    list.add("2");
    list.add("2");
    list.add("2");
    list.add("3");
    list.add("4");
    list.add("5");

    // 这样做肯定抛异常,非法访问数据越界
    int len = list.size();
    for (int i = 0; i < len; i++) {
    if("1".equals(list.get(i))){
    list.remove(i);
    }
    }

    //Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 4, Size: 4
    //改进后,删除元素后更新List长度,更新循环下标。但是这样的可读性很差。而且不适用于多线程场景
    int len = list.size();
    for (int i = 0; i < len; i++) {
    if("1".equals(list.get(i))){
    list.remove(i);
    --len;
    --i;
    }
    }
    //另外一种方式,这种看起来好读多了。。。
    Iterator<String> sListIterator = list.iterator();
    while(sListIterator.hasNext()){
    String e = sListIterator.next(); //注意了
    if(e.equals("1")){
    sListIterator.remove();
    }
    }

什么是 Java 序列化?

  • 序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。
  • 反序列化的过程,则是和序列化相反的过程。
  • 对于不想进行序列化的变量,使用transient关键字修饰。
    • transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。

介绍Java 反射

  • 反射(Reflection)是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。

  • 通过反射,可以在程序运行时访问Java对象的成员变量、方法、构造方法。

  • 反射的缺点

    • 性能开销 - 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用程序中频繁调用的代码段中避免。
    • 破坏封装性 - 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
    • 内部曝光 - 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。
  • 反射的实现方式:

  • 第一步:获取Class对象,有4中方法:

    • 1)Class.forName(“类的路径”);
    • 2)类名.class
    • 3)对象名.getClass()
    • 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象