Java面试准备之Java基础

Java面试需要准备的东西很多,包括但不限于Java基础、常用API类、面向对象思想、经典jdk源码分析,集合类、并发与多线程、IO/NIO/AIO、反射与代理、JVM、TCP/IP、数据库、redis、数据结构与算法、设计模式、Git、Maven、分布式/微服务/云服务、前后端分离、vue全家桶、spring全家桶等等。要想每一个知识点都掌握显然难度不小,但在平常学习过程中对一些面试经常会遇到的问题做点总结,深扒一下底层原理,就可以在面试中脱颖而出。本篇内容主要是和面向对象有关的知识点,相关问题在实际面试中出现的频率非常高,值得记录下来跟道友一起分享。

1 Java的自动装箱拆箱过程

1.1 Java中基本数据类型与包装类型

Java基本数据类型有八种,主要分为四类:

​ 字符类型: char

​ 布尔类型: boolean

​ 整数类型: byte、short、int、long

​ 浮点类型: float、double

在Java语言中new一个对象是存储在堆里的,我们通过栈中的引用来使用这些对象;所以,对象本身来说是比较消耗资源的。而基本数据类型的变量不需要用new创建,他们不会在堆上创建,而是直接在栈内存中存储,因此会更加高校。

Java语言是一个面向对象的语言,但是Java中的基本数据类型却不是面向对象的,这在实际使用时存在许多不便,为了解决弥补这个不足,在设计每个基本数据类型时设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。包装类均位于java.lang包,包装类和基本数据类型的对应关系如下表所示。

基本数据类型 包装类
byte Byte
boolean Boolean
short Short
char Character
int Integer
long Long
float Float
double Double

Java语言在编程时很多地方都需要使用对象而不是基本数据类型。比如在集合类中我们无法将int、double等基本数据类放进去,因为集合容器要求元素是Object类型的。为了让基本数据类型也具有对象的特征,就出现了包装类型,它相当于将基本数据类型包装起来,使得它具有了对象的性质,并为其添加了属性和方法,丰富类基本类型的操作。

1.2 Java中的自动装箱与自动拆箱的概念及其实现原理

有时候需要在基本数据类型和包装类之间进行转换,将基本数据类型转换成包装类就是装箱,反之就是拆箱。在Java SE5中为了减少开发人员的工作,Java提供了自动拆箱与自动装箱功能,通过代码比较一下实现自动前后的区别。

1
2
3
Integer i = new Integer(10);//装箱
Integer i = 10;//自动装箱
int b = i; //自动拆箱

实际上,自动装箱都是通过valueOf()方法来实现的,自动拆箱都是通过包装类对象的xxxValue()来实现的。

1.3 自动拆装箱的应用场景与存在的问题

将基本数据类型放入集合类时、包装类型和基本数据类型的大小比较、包装类型的运算、三目运算符的使用、函数参数与返回值,这几种情况下都会发生自动拆装箱。

Java SE的自动拆装箱还提供了一个和缓存有关的功能,我们先来看一下代码,猜测一下输出结果:

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

public static void main(String... strings) {

Integer integer1 = 3;

Integer integer2 = 3;

if (integer1 == integer2)

System.out.println("integer1 == integer2");

else

System.out.println("integer1 != integer2");

Integer integer3 = 300;

Integer integer4 = 300;

if (integer3 == integer4)

System.out.println("integer3 == integer4");

else

System.out.println("integer3 != integer4");
}

我们普遍认为上面的两个判断的结果都是false。虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个if判断都是false的。在Java中,==比较的是对象应用,而equals比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回false。奇怪的是,这里两个类似的if条件判断返回不同的布尔值。

1
2
integer1 == integer2
integer3 != integer4

原因就和Integer中的缓存机制有关。在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。我们只需要知道,当需要进行自动装箱时,如果数字在-128至127之间时,会直接使用缓存中的对象,而不是重新创建一个对象。

自动拆装箱是一个很好的功能,大大节省了开发人员的精力,不再需要关心到底什么时候需要拆装箱。但是,他也会引入一些问题。包装对象的数值比较,不能简单的使用==,虽然-128到127之间的数字可以,但是这个范围之外还是需要使用equals比较。前面提到,有些场景会进行自动拆装箱,同时也说过,由于自动拆箱,如果包装类对象为null,那么自动拆箱时就有可能抛出空指针异常。

2 为什么有时候需要重写hashcode和equal方法,作用是什么?

2.1 equals方法

equals方法在Object类中默认的实现方式是return this == obj 。只有当this和obj引用同一个对象,才会返回true。而我们往往需要用equals来判断2个对象是否等价,而非验证他们的唯一性。这样我们在实现自己的类时,就要重写equals方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//重写equals方法
class Test{
private int num;
private String date;

public boolean equals(Object obj){
if(this == obj)
return true;
if((obj == null) || (obj.getClass() != this.getClass()))
return false;
//程序执行到这里说明obj和this同类且非null。
Test test = (Test) obj;
return num == test.num && (data == test.data || (data != null) && data.equals(test.data()));
//这里重写hashCode()方法
public int hashcode(){

}
}
}

2.2 hashCode方法

hashCode()方法返回对象的散列码,返回值是int类型的散列码。对象的散列码是为了更好的支持基于哈希机制的Java集合类,例如Hashtable,HashMap,HashSet等。上面代码块中Test类对象有2个字段,num和data,这两个字段代表了对象的状态,他们也用在equals方法中作为评判的依据。那么在hashCode方法中这两个字段也要参与hash值的运算,作为hash运算的中间参数。这点很关键,这是为了遵守两个对象equals,那么hashCode一定相同的规则,因此重写了euqals()方法的对象必须同时重写hashCode()方法。

重写hashCode方法时,一致性的约定是:

  • 在某个运行期间,只要对象的变化不会影响equals方法的决策结果,那么在这个期间,无论调用多少次hashCode,都必须返回一个散列码。
  • 如果2个对象通过equals调用后返回是true,那么这2个对象的hashCode方法也必须返回同样的int型散列码。
  • 如果2个对象通过euqals调用后返回是fasle,它们的hashCode返回的值允许相同(然而hashCode返回独一无二的散列码,会让存储在这个对象的hashtables更好地工作)。

重写hashCode()方法时除了上述一致性约定,还有一下几点需要注意:

  • 返回的hash值是int型的,防止溢出。
  • 不同的对象返回的hash值应该尽量不同。
  • 《Java编程思想》中提到一种情况:“设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都因该产生同样的值。如果在将一个对象用put()添加进HashMap时产生一个hashCode值,而用get()取出时却产生了另一个hashCode值,那么就无法获取该对象。所以如果你的hashCode()方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时hashCode()方法就会生成一个不同的散列码”。

3 System.out.println(new mianshi())打印的是什么?

这题实质上是在考察Object类中的默认方法–toString()。Java中所有对象都是继承自Obejct,自然继承了toString方法,在当使用System.out.println()里面为一个对象的引用时,自动调用toStirng方法将对象打印出来。如果重写了toString()方法则调用重写的toString()方法。这里如果没有对mianshi类的toString()方法进行重写,那么将会输出 mianshi@hashcodea。

4 syn锁与lock锁的区别

4.1 synchronized的相关概念与实现原理

4.1.1synchronized的基本概念

在并发编程中存在线程安全问题,主要原因是:

  • 存在共享数据
  • 多线程共同操作共享数据

synchronized是Java提供的一个并发控制的关键字。主要有两种用法,分别是同步方法和同步代码块。也就是说,synchronized既可以修饰方法也可以修饰代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author Hollis 18/08/04.
*/
public class SynchronizedDemo {
//同步方法
public synchronized void doSth(){
System.out.println("Hello World");
}

//同步代码块
public void doSth1(){
synchronized (SynchronizedDemo.class){
System.out.println("Hello World");
}
}
}

synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问。

4.1.2 synchronized的实现原理

首先需要使用Javap来反编译以上代码,结果如下(部分无用信息过滤掉了):

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
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return

反编译后,我们可以看到Java编译器为我们生成的字节码。在对于doSthdoSth1的处理上稍有不同。也就是说。JVM对于同步方法和同步代码块的处理方式不同。对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorentermonitorexit两个指令来实现同步。

  • 方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
  • 同步代码块使用monitorentermonitorexit两个指令实现。可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

实现原理总结:同步方法通过ACC_SYNCHRONIZED关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。同步代码块通过monitorentermonitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得所锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。即可重入锁。

4.1.2 monitor是什么?怎么定义的?一个什么标志?在哪里标志的?

我们可以把monitor理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。 Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用

4.1.3 synchronized的三种应用方式?

Java中每一个对象可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法(实例方法),锁是当前实例对象,进入同步代码前要获得当前实例的锁。

    ======多个线程访问同一个对象的同一个方法======

    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
    public class synchronizedTest implements Runnable {
    //共享资源
    static int i =0;
    /**
    * synchronized 修饰实例方法
    */
    public synchronized void increase(){
    i++;
    }
    @Override
    public void run(){
    for (int j =0 ; j<10000;j++){
    increase();
    }
    }

    public static void main(String[] args) throws InterruptedException {
    synchronizedTest test = new synchronizedTest();
    Thread t1 = new Thread(test);
    Thread t2 = new Thread(test);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);
    }
    }
    /**
    console:
    2000

    Process finished with exit code 0
    ============================================================
    分析:当两个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法,但是可以访问非synchronized修饰的方法
    */

    ======一个线程获取了该对象的锁之后,其他线程来访问其他synchronized实例方法现象======

    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
    45
    46
    47
    48
    49
    50
    51
    52
    53
     public synchronized void method1() {
    System.out.println("Method 1 start");
    try {
    System.out.println("Method 1 execute");
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Method 1 end");
    }

    public synchronized void method2() {
    System.out.println("Method 2 start");
    try {
    System.out.println("Method 2 execute");
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Method 2 end");
    }


    public static void main(String[] args) {
    final syncTest test = new syncTest();

    new Thread(new Runnable() {
    @Override
    public void run() {
    test.method1();
    }
    }).start();

    new Thread(new Runnable() {
    @Override
    public void run() {
    test.method2();
    }
    }).start();
    }
    /**
    console:
    Method 1 start
    Method 1 execute
    Method 1 end
    Method 2 start
    Method 2 execute
    Method 2 end

    Process finished with exit code 0
    ============================================================
    可以看出其他线程来访问synchronized修饰的其他方法时需要等待线程1先把锁释放
    */

    =一个线程获取该对象的锁之后,其他线程来访问其他非synchronized实例方法现象(去掉method2的锁)=

    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
    45
    46
    47
    48
    49
    50
    51
    52
    53
    public synchronized void method1() {
    System.out.println("Method 1 start");
    try {
    System.out.println("Method 1 execute");
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Method 1 end");
    }

    public void method2() {
    System.out.println("Method 2 start");
    try {
    System.out.println("Method 2 execute");
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Method 2 end");
    }


    public static void main(String[] args) {
    final syncTest test = new syncTest();

    new Thread(new Runnable() {
    @Override
    public void run() {
    test.method1();
    }
    }).start();

    new Thread(new Runnable() {
    @Override
    public void run() {
    test.method2();
    }
    }).start();
    }
    /**
    console:
    Method 1 start
    Method 1 execute
    Method 2 start
    Method 2 execute
    Method 2 end
    Method 1 end

    Process finished with exit code 0
    ============================================================
    当线程1还在执行时,线程2也执行了,所以当其他线程来访问非synchronized修饰的方法时是可以访问的
    */

    ====当多个线程作用于不同的对象====

    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
    //共享资源
    static int i =0;
    /**
    * synchronized 修饰实例方法
    */
    public synchronized void increase(){
    i++;
    }
    @Override
    public void run(){
    for (int j =0 ; j<10000;j++){
    increase();
    }
    }
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new synchronizedTest());
    Thread t2 = new Thread(new synchronizedTest());
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);
    }
    /**
    console:
    Method 1 start
    Method 1 execute
    Method 2 start
    Method 2 execute
    Method 2 end
    Method 1 end

    Process finished with exit code 0
    ============================================================
    因为两个线程作用于不同的对象,获得的是不同的锁,所以互相并不影响
    **此处思考一个问题:为什么分布式环境下synchronized失效?如何解决这种情况?**
    */
  • 静态同步方法,锁是当前类的class对象,进入同步代码前要获得当前类对象的锁。

    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
    public class synchronizedTest implements Runnable {
    //共享资源
    static int i =0;
    /**
    * synchronized 修饰实例方法
    */
    public static synchronized void increase(){
    i++;
    }
    @Override
    public void run(){
    for (int j =0 ; j<10000;j++){
    increase();
    }
    }

    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new synchronizedTest());
    Thread t2 = new Thread(new synchronizedTest());
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);
    }
    /**
    console:
    2000

    Process finished with exit code 0
    ============================================================
    由例子可知,两个线程实例化两个不同的对象,但是访问的方法是静态的,两个线程发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。
    */
  • 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    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
     public class synchronizedTest implements Runnable {
    static synchronizedTest instance=new synchronizedTest();
    static int i=0;
    @Override
    public void run() {
    //省略其他耗时操作....
    //使用同步代码块对变量i进行同步操作,锁对象为instance
    synchronized(instance){
    for(int j=0;j<10000;j++){
    i++;
    }
    }
    }
    public static void main(String[] args) throws InterruptedException {
    Thread t1=new Thread(instance);
    Thread t2=new Thread(instance);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);
    }
    }
    /**
    console:
    2000

    Process finished with exit code 0
    ============================================================
    为什么要同步代码块呢?在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。
    分析:将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁。
    */

4.2 lock锁的是实现原理?是否可以重入,怎么重入?

4.3 syn锁与lock锁的区别

4.4 锁升级的概念以及一般过程

5 volatile关键字的作用

-------------本文结束感谢您的阅读-------------
zouzou wechat
欢迎您扫一扫上面的微信号,添加我的私人微信!
坚持原创技术分享,您的支持将鼓励我继续创作!
  • 本文标题: zouzou
  • 文章作者: Java面试准备之Java基础
  • 发布时间: 2019年08月21日 - 00:00
  • 最后更新: 2019年08月23日 - 19:51