2019 Java面试基础知识整理——持续更新

文章目录

Java

HashMap

hash碰撞解决方法

(1) 开放定址法

Hash碰撞之后就向下寻找空的存储空间

(2) 链地址法(拉链法)

数组和链表结合,碰撞之后就插入链表(静态数组和动态数组的结合

(3) 再hash法

碰撞之后就再生成一hash表

HashMap扩容

(1)为什么要扩容,随着哈希表插入的数据越来越多,查找效率越来越低(哈希碰撞越来越多)

(2)直接扩大,重新生成新的最大值,重新生成hashcode

(3)Java大于数组长度的0.75,开始扩容,原来数组的两倍大

HashMap链表查找效率低怎么解决:

JDK1.8以后链表长度大于8时,自动转换成红黑树

HashCode

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值,方便字符串和类型进行比较

两个对象equal返回true,那么两个对象的hashCode()值也相同,反之不亦然!

HashCode 就是Hash值,Java中一般选用 2 N 2^N 来做模,好处就是取模速度快,原理如下:

static int Fun(int x,int length){
    return x & (length - 1);     //对(2^N-1)来说,取模直接&即可
}

为什么需要HashCode——方便!
总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。这里就引出一个问题:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?

这就是Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。

于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上,初学者可以简单理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。

这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

equals

equals在Object对象中直接比较两个类的引用地址,而在一些类型如String 、Math、Integer、Double中,对equals进行了重写,调用equals的时候是对内容进行比较,而不是简单的对引用地址进行比较。

  1. 自反性
x.equals(x) == true;
  1. 对称性
x.equals(y) == y.equals(x);
  1. 传递性
if(x.equals(y) && y.equals(z)){
    x.equals(z) == true;
}
  1. 一致性
x.equals(y) == x.equals(y);
  1. 与null比较
x.equals(null) == false;//x不为null

参考文章

equals 和 ==

  1. 对于基本类型,用==来判断两个值是否相等
  2. 对引用类型(对象),用==来判断是否是同一个引用,而用equals来判断二者内容是否相等(例如String,此时的equals被重写了)

sleep()和wait()的区别

(1)本质区别是sleep()不会释放同步锁,wait()释放同步锁

(2)sleep是Thread的方法,wait()是Object的方法

(3)wait(),notify 和 notifyAll 必须要在同步控制的方法中使用,而sleep()可以子任何地方使用

(4)sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

(5)由于sleep不会释放同步锁,容易导致死锁问题,因此更推荐用wait

同步锁

产生原因

  1. 线程之间存在共享数据。
  2. 线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。多线程情况下就会产生并发问题。

意义

当多个线程共用一块资源的时候,会出现资源抢占,加了同步锁之后,使得同一段时间只有一个线程会获得资源,其他线程处于等待状态。

主要实现方式有

synchronized,Lock

synchronized使用

synchronized是Java关键字,在JVM层面实现线程锁

当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?

不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

对于一个类,在其synchronized方法A内是永远无法调用他的synchronized方法B的,因为同时只能有一个锁,B方法要等A方法释放锁才能调用。

Lock的使用

Lock是一个接口

public interface Lock {

    void lock();
 
    void lockInterruptibly() throws InterruptedException;
 
    boolean tryLock();
 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
 
    void unlock();
 
    Condition newCondition();
}

lock()

如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try-catch块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

Lock lock = new ReentrantLock();  
lock.lock();
try {
    for(int i=0;i<5;i++) {
        arrayList.add(i);   
    }
} catch (Exception e) {

}finally {
    lock.unlock();
}

tryLock()

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

tryLock(long time, TimeUnit unit)

方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。使用方法同上。

lockInterruptibly

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

synchronized和Lock的区别

  1. synchronized是Java关键字,是在JVM层面控制同步的。Lock是一个接口,在代码JDK层面进行控制的
  2. synchronized在出现异常的时候,会主动去释放线程占用的锁,因此不会造成死锁现象;而Lock在出现异常的情况必须要在catch或者finally中调用unLock来释放锁资源,如果没用调用就会产生死锁。
  3. synchronized自动释放锁资源,Lock要手动释放
  4. Lock可以中断响应,让线程中断去做其他的事情,而synchronized不行
  5. Lock可以处理读写问题,而synchronized不行(举个例子,两个线程对同一个文件同时进行读和写的操作,会产生冲突;而同时进行读的操作,则不会产生冲突,然而如果用了synchronized关键字来实现,就会使得当前只有一个线程能够进行读操作,而通过Lock就不会发生冲突现象)
  6. Lock可以获得锁状态,而synchronized不行

synchronized是悲观锁,Lock是乐观锁

Lock的优势

  1. 可以查看锁状态
  2. 可以处理读写问题(两个线程可以同时进行读的操作)
  3. Lock可以中断响应,让线程去做其他事情。

synchronized和volatile

线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。多线程情况下就会产生并发问题。

synchronized保护代码在同一时刻只能有一个线程执行,这是原子性有序性;同时synchronized还会创造一个内存屏障,保证了所有CPU操作都会直接刷到主内存中,这是可见性

volatile对所有volatile变量的读写都直接在主内存进行,这样就保证了可见性

volatile可以修饰一般对象或者数组,但是要保证引用正确的情况下才行

volatile不能用来修饰final类型的变量。

区别

  1. volatile能修饰基础变量(int,long,float…),synchronized不仅可以修饰变量、类型,还能修饰函数、代码块。
  2. volatile保证数据的可见性、有序性;synchronized保证可见性、原子性、有序性。
  3. volatile不会造成线程阻塞;synchronized可能造成线程堵塞
  4. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

GC垃圾回收机制

堆内存、栈内存和方法区

  1. 堆内存存储基本类型(int、float、double、char[Unicode、16位、和C的区别]、boolean、short、byte)和对象的引用,不在GC的管理范围内。
  2. 栈内存主要是有new关键字生成的对象内存,需要通过GC来管理。
  3. 方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据。
  4. 程序中的字面量例如100,“Hello”等常量都是放在常量池,常量池存在方法区。
  5. 栈内存操作速度快,但是一般内存较小。
  6. 栈内存用完了会引发StackOverflowError,而堆内存和方法区用完了会引发OutOfMemoryError

GC判断内存回收算法

  1. 引用计数法:对一个对象添加引用计数器,引用就加1,引用失效就减1,引用个数等于0就等待回收内存。缺点:无法释放循环引用的对象。
  2. 可达性分析法:从根节点进行搜索,如果对象内存不可达,则证明无用对象,可进行释放

GC回收算法

  1. 标记-清除法:标记需要清除的对象,统一进行回收。缺点:效率低,容易造成内存的碎片化
  2. 复制法:这个方法把内存分为两块区域,每次仅使用其中的一块区域。垃圾回收的时候,把正在使用的对象复制到另外一片内存区域中去。缺点:需要两倍的内存空间,拷贝效率低
  3. 标记整理法:针对对象存活率高而进行复制效率低的问题,该算法在让内存向一端移动,使得内存区域不产生碎片。
  4. 分代收集算法:
  • 目前Java虚拟机使用的算法
  • 根据对象的存活周期不同,将内存分为新生代,老年代,永久代
  • 新生代和老年代都在java堆,永久代在方法区,对GC来说主要管理新生代和老年代
  • 新生代:采用复制算法
  • 老年代:采用标记-清除和标记-整理法来进行垃圾回收
  • 永久代:采用标记-清除和标记-整理法来进行垃圾回收

GC什么时候进行回收:

  1. 会在cpu空闲的时候自动进行回收
  2. 在堆内存存储满了之后
  3. 主动调用System.gc()后尝试进行回收

永久带垃圾回收

  • 判断废弃常量:一般是判断没有该常量的引用。

  • 判断无用的类:要以下三个条件都满足

    • 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
    • 加载该类的 ClassLoader 已经被回收
    • 该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

参考资料

Java对象声明周期

  1. 创建阶段
  2. 应用阶段
  3. 不可见阶段
  4. 不可达阶段
  5. 收集阶段
  6. 终结阶段
  7. 对象空间重分配阶段

单例模式

懒汉模式

public class SingleTask{
    private static SingleTask instance;

    private SingleTask(){

    }

    public static synchronized SingleTask getInstance(){
        if(instance == null){
            instance = new SingleTask();
        }
        return instance;
    }
}

饿汉模式

public class SingleTask{
    private static SingleTask instance;

    static{
        instance = new SingleTask();
    }

    private SingleTask(){

    }

    public static synchronized SingleTask getInstance(){
        return instance;
    }
}

双重校验模式

为什么要双重校验,因为在new创建实例的时候,一般执行顺序是

  1. memory=allocate();// 分配内存 相当于c的malloc
  2. ctorInstanc(memory) //初始化对象
  3. s=memory //设置s指向刚分配的地址

正常执行顺序1-2-3,但是有可能会出现1-3-2的情况,一旦只执行1-3时,恰好有一个新的线程对单例进行访问,这时候可能没有完成单列的初始化,造成程序错误。加上volatile就可以保证执行顺序是正确的。

public class SingleTask{
    private volatile static SingleTask instance;

    private SingleTask(){

    }

    public static SingleTask getInstance(){
        if(instance == null){
            synchronized (SingleTask.class){
                if(instance == null){
                    instance = new SingleTask();
                }
            }
        }
        return instance;
    }
}

Java线程

创建线程

继承Thread方法

class MyThread extends Thread{
    public void run(){
        //...
    }
}

Runnable实现

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

通过 Callable 和 Future 创建线程

实现了call()方法,常用于线程池。

class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() {
        return 0;
    }
}

MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
new Thread(futureTask).start();

三种方式比较

  1. Runnable和Callable是接口,还可以继承其他的类
  2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
  3. Callable可以通过Future查看线程的执行状态

线程生命周期

  1. 新建状态:使用new关键字建立线程以后到调用start之前,就处于新建状态
  2. 就绪状态:当现场调用start方法之后,就进入就绪状态,等待JVM分配CPU资源
  3. 运行状态:CPU分配资源后,开始执行run()方法,进入运行状态
  4. 阻塞状态:
  • 等待阻塞:在运行状态中执行了wait()方法,使线程进入等待阻塞
  • 同步阻塞:获取同步锁失败(资源被其他线程占用)
  • 其他阻塞:调用sleep()方法或者join()方法
  1. 死亡状态:线程任务执行完毕,该线程自动切换的终止状态

优先级

setPriority()方法

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
优先级取值范围1-10。

虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。

sleep()

  1. sleep是静态方法,sleep的调用只对调用时所在的线程有效,而非调用的实例对象。
  2. 调用sleep后线程进入阻塞状态,待sleep结束以后线程进入就绪状态。
Thread thread = new Thread();
thread.sleep();
Thread.sleep();

yield()

  1. yield是静态方法,yield的方法让线程暂停,同时让出所占用的CPU资源,同时当前线程进入线程就绪状态,等待系统分配资源。
  2. 与sleep的区别就是直接进入就绪状态,不会进入堵塞状态

join()

thread0.join()方法阻塞调用此方法的线程(thread1),直到线程thread0完成,thread1再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。

如何结束一个线程

  1. 任务执行完自动结束线程
  2. 利用线程共享资源来改变循环条件结束线程

线程同步

synchronized 和 Lock

线程通信

  1. wait(),线程执行wait后,释放线程资源,进入线程阻塞状态,处于冻结状态。wait可以设置自动醒来时间。
  2. notify(),线程运行时,内存中会生成一个内存池,调用notify会找到线程池中第一个被冻结的线程。
  3. notifyAll(),唤醒线程池中所有冻结的线程

线程池

  1. 降低系统开销:创建线程对系统资源消耗较大,线程池在创建时生成,只占用一定的内存资源,不占用CPU资源。利用已创建的线程来降低线程创建和销毁时产生的消耗。
  2. 提高响应速度:当有任务达到时,线程池自动唤醒空闲的线程开始执行任务,不需要等待线程创建。
  3. 提高线程的可管理性:线程不能无限制创建,利用线程池来统一分配和管理,提高系统资源利用率。

参考文章

线程参考文章

Object方法

  1. getClass() 返回此Object运行时的类
  2. hashCode() 返回对象的哈希值
  3. equal(Object obj) 判断两个对象是否相等
  4. clone() 返回此对象的一个副本
  5. toString() 返回对象的字符串
  6. notify() 唤醒在此对象监视器上等待的单个线程
  7. notifyAll() 唤醒在此对象监视器上等待的所有线程
  8. wait() 使当前对象的线程进行等待
  9. finalize() 垃圾回收时调用该方法
public final native Class<?> getClass()    //返回此 Object 运行时的类
 
public native int hashCode()    //返回对象的哈希码
 
public boolean equals(Object obj)    //判断其他对象与此对象是否“相等”
 
protected native Object clone() throws CloneNotSupportedException    //创建并返回此对象的一个副本
 
public String toString()    //返回对象的字符串表示
 
public final native void notify()    //唤醒在此对象监视器上等待的单个线程
 
public final native void notifyAll()    //唤醒在此对象监视器上等待的所有线程
 
public final native void wait(long timeout) throws InterruptedException    //使当前对象的线程等待 timeout 时长
 
public final void wait(long timeout, int nanos) throws InterruptedException    //使当前对象的线程等待 timeout 时长,或其他线程中断当前线程
 
public final void wait() throws InterruptedException    //使当前对象的线程等待 
 
protected void finalize() throws Throwable {}    //垃圾回收时调用该方法

深克隆和浅克隆

在Object基类中,有一个方法叫clone,产生一个前期对象的克隆,克隆对象是原对象的拷贝,由于引用类型的存在,有深克隆和浅克隆之分,若克隆对象中存在引用类型的属性,深克隆会将此属性完全拷贝一份,而浅克隆仅仅是拷贝一份此属性的引用

由于Object类中clone时protected,子类继承时需要重写。

浅克隆clone()的实现

class Test implements Cloneable{
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

深克隆的实现

  1. 重写对象的clone()以及所有引用对象的clone()方法,调用clone()方法时会进行深层递归调用克隆,使得对象的每个引用都进行了克隆。
  2. 实现Serializable接口,通过对象的序列化和反序列化实现
    class Test implements Serializable {
        public Test clone(){
            Test t1 = this;
            Test t2 = null;

            PipedOutputStream out = new PipedOutputStream();
            PipedInputStream in = new PipedInputStream();

            try{
                in.connect(out);
            }catch (IOException e){
                e.printStackTrace();
            }

            try{
                ObjectOutputStream bo=new ObjectOutputStream(out);
                ObjectInputStream bi = new ObjectInputStream(in);
                
                bo.writeObject(t1);
                t2 = (Test)bi.readObject();
            }catch (Exception e){
                e.printStackTrace();
            }

            return t2;
        }
    }

参考文章

参考文章

Java继承

  • 子类拥有父类非private方法(非同包继承,不能继承default方法)
  • 子类拥有自己的属性和方法,子类可以对父类进行扩展
  • 子类可以重写父类的方法,也可也在父类方法的基础上进行改进
  • 对象继承(extends)只能单继承,接口继承(implements)可以多继承
  • super关键字来实现对父类函数的调用
  • this关键字指向实例本身
  • 声明为final的类或者方法是无法继承的
  • 子类无法继承父类的构造函数,但是如果父类构造函数带参,子类必须显式的通过super关键字进行调用,若父类构造函数不带参,系统会自动调用父类构造函数。
  • super 语句必须是子类构造方法的第一条语句(如果没写,系统会自动加上)
  • 当一个类没有继承的两个关键字,则默认继承Object
  • 子类重写函数的的参数列表必须相同,返回值可以不同(但是子类返回值必须是父类返回值的派生类)
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类的访问权限不能比父类更低
  • 变量不能被重写
  • 父类引用指向子类对象,无法调用子类独有的方法

举个例子

public class Main {
    static public class Father {
        public String name = "father";

        public void getName(){
            System.out.println(name);
        }
    }

    static public class Son extends Father {
        public String name = "son";

        public void getName(){
            System.out.println(name);
        }
    }

    public static void main(String[] args) {
        Father father = new Son();
        System.out.println(father.name);
        father.getName();
    }
}

输出:

father
son

面向对象的特征

  • 抽象
  • 封装:将数据隐藏起来,对数据的访问只能通过特定的接口
  • 继承
  • 多态:重载和重写

重载和重写的区别

  1. 重写是子类对父类实现过程进行重写编写,返回值、形参都不能改变
  2. 重载发生在同一个类中,有两个同名的方法但是参数列表不同即认为重载,此时方法的返回值可以不同。

为什么不能根据返回值来区分重载

因为调用的时候无法确定类型信息,编译器不知道你要调用哪个函数。

public,private,protecter,default

Java精度转换

Java的精度只能向上转,向下转会报错

int x = 1;
float y = 2;

x = x + y; //错误
y = x + y; //正确

又如:

short x = 1;//正确
x += 1;//正确
x = x + 1;//错误,此时1为int型

int和Integer有什么区别

  • int是基本类型,Integer是对象
  • 基本类型的初始值是0,对象初始值是null
  • int内存在栈内存,Integer在堆内存,在栈中保留对象的引用

从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换(即可以用==对int和Integer的值进行比较

装箱、拆箱什么含义

装箱就是自动将基本数据类型转换为包装器类型,拆箱就是自动将包装器类型转换为基本数据类型,方便基本类型和包装类型的转换。

&和&&的区别

  1. &是位运算符,&&是短路与运算符
  2. &可以进行位运算或者逻辑运算
  3. && 对两边进行逻辑与操作,若发现左边为false,右边程序将不执行

char

  1. 一个char类型占2个字节(16位)
  2. Java中的char编码是Unicode
  3. char类型是可以存储一个中文字的

##Java抽象类和接口的区别

抽象类

  1. 抽象类不能实例化
  2. 抽象类必须被继承
  3. 抽象类里可以没有抽象方法
  4. 抽象类必须用abstract来修饰
  5. 抽象类可以有构造函数,只能用于子类调用

抽象方法

  1. 抽象方法没有方法体,只能被继承
  2. 有抽象方法的类只能被定义为抽象类(如没加abstract,系统会默认加上)
  3. 抽象方法用abstract来修饰

接口

  1. 接口并不是类
  2. 接口可以多继承,此时接口并不是被继承,而是被类实现了
  3. 接口无法实例化,但是可以被实现,被实现的接口必须实现接口内的所有方法
  4. 接口的方法默认为抽象方法
  5. 接口的方法必须是public
  6. 接口没有构造方法
  7. 接口中的变量必须是static和final
  8. 接口中的方法没有方法体
  9. Java1.8以后可以定义default方法

接口和抽象类的区别

  1. 抽象类可以有私有方法和变量,接口的所有方法和变量必须是公有的
  2. 抽象类的方法并不都是抽象方法,可以有方法体
  3. 抽象类可以有构造函数,接口没有
  4. 抽象类只能单继承(extends),接口可以多继承(implements)
  5. 接口强调特定功能的实现,而抽象类强调所属关系
  6. 抽象类可以有main函数并且可以运行,接口不行

Java内存泄漏

一般来说,有GC管理,不会存在内存泄漏问题。但是有可能存在一些长时间存在的对象,这些对象持有一些无用的变量引用,GC在进行垃圾回收的时候,就会跳过这些引用内存不进行回收,造成内存泄漏,严重时会造成OutOfMemoryError。

长生命周期的对象持有短生命周期的对象。短周期对象就无法及时释放。

静态变量和实例的区别

  1. 静态变量由static修饰,本质上属于一个单独的类对象
  2. 不论创建多少个类对象,静态对象在内存中只存在一个拷贝,静态变量在编译阶段即进行赋值操作,后面创建类对象不再进行赋值操作。

final

  1. 修饰基本类型,表示数值不能改变
  2. 修饰对象,对象的引用不能改变,对象内部方法还是可以调用
  3. 修饰方法,该方法不能被重写
  4. 修饰类,该类不能被继承

为什么匿名内部类要用final来修饰

一方面,由于方法中的局部变量的生命周期很短,一旦方法结束变量就要被销毁,为了保证在内部类中能找到外部局部变量,通过final关键字可得到一个外部变量的引用;另一方面,通过final关键字也不会在内部类去做修改该变量的值,保护了数据的一致性。

try里面加return,finally里面代码会不会执行

代码会执行,在return前执行

final、finally、finalize的区别

  1. final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final的方法也同样只能使用,不能在子类中被重写。
  2. finally:通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
  3. finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。

Error和Exception有什么区别

Error表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。

String

String特性及使用

  • String不是基本类型。
  • String是final类型,不能被继承。
  • String是只读字符串,每次对String字符串进行改变操作实际上是创建一个新的String对象。
  • 字符串的+操作其本质是创建了StringBuilder对象进行append操作,然后将拼接后的StringBuilder对象用toString方法处理成String对象。
String s = new String("abc"); //此时创建了一个常量对象和一个堆对象

StringBuffer特性及使用

//变量申明
StringBuffer str = new StringBuffer("1000");
//在变量尾部增加字符串
str.append("abcde");
//转String
String str2 = str.toString();

String、StringBuffer和StringBuilder区别

  1. 三者在执行速度方面的比较:StringBuilder > StringBuffer > String
  2. StringBuffer是线程安全的适用于多线程下操作大量数据;StringBuilder不是线程安全的,适合单线程下操作大量数据

Java 容器

Collection接口

是高度抽象出来的集合,它包含了集合的基本操作和属性。Collection包含了List和Set两大分支,还有Queue。

List 接口

是一个有序的队列,每一个元素都有它的索引。第一个元素的索引值是0。List的实现类有LinkedList, ArrayList, Vector, Stack。

LinkedList 和 ArrayList 的区别

实现的数据结构不同,LinkedList用的双向链表,ArrayList是通过数组(动态数组)实现,转化成数组和链表的区别和适用场景。

数组和链表
  1. 数组查找快O(1),链表O(n)
  2. 链表插入、删除操作快,数组慢
  3. 链表占用更多的内存(指针引用),数组无
什么场景下更适宜使用 LinkedList,而不用ArrayList
  1. 需要经常查找数据时,ArrayList更加快速。
  2. 需要经常插入和删除数据时,LinkedList更加快速。

ArrayList 和 Vector区别

  1. 二者都是用数组的方式储存数据
  2. Vector方法内加了synchronized修饰,因此Vector是线程安全的,但是性能上较ArrayList要更差。
  3. Vector是线程安全的,但是性能较差,一般情况下使用ArrayList,除非特殊需求。

Stack继承自Vector

Vector能用的方法,Stack都能用。一般不建议使用。

不推荐使用Vector的原因

Vector属于遗留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器。

synchronizedList

解决ArrayList不是线程安全的问题。

//转换为线程安全的ArrayList
List<String> list = Collections.synchronizedList(new ArrayList<>());

//加同步锁的原因是Iterator此时不是线程安全的
synchronized (list) {
    Iterator i = list.iterator(); // Must be in synchronized block
    while (i.hasNext())
        foo(i.next());
}

Set 接口

是一个不允许有重复元素的集合。 Set的实现类有HastSet和TreeSet。HashSet依赖于HashMap,它实际上是通过HashMap实现的;TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。

HashSet 和 TreeSet

  1. HashSet 通过HashMap来实现
  2. TreeSet 通过红黑树来实现

Collection和Collections的区别?

答:Collection是一个接口,它是Set、List等容器的父接口;Collections是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。

Map 接口

是一个映射接口,即key-value键值对。Map中的每一个元素包含“一个key”和“key对应的value”。 AbstractMap是一个抽象类,它实现了Map中大部分的API。而HashMap,TreeMap,WeakHashMap都是继承AbstractMap。Hashtable虽然继承Dictionary,但是实现的Map接口。

HashMap 和 TreeMap

  1. HashMap 通过Hash表来实现
  2. TreeMap 通过红黑树来实现

TreeMap和TreeSet在排序时如何比较元素?

TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。

public class Test implements Comparable<Test> {
    private int val; 

    @Override
    public int compareTo(Test o) {
        return this.age - o.age; 
    }

}

HashTable

  • 和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。
  • 实现线程安全的方式是在修改数据时锁住整个HashTable,访问效率低下(一是每次访问需要获取线程锁,二是多线程操作时产生阻塞)。
  • Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
  • Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。

HashTable和HashMap的区别

  • HashMap基于AbstractMap类,实现了Map、Cloneable(能被克隆)、Serializable(支持序列化)接口; 非线程安全;允许存在一个为null的key和任意个为null的value;采用链表散列的数据结构,即数组和链表的结合;初始容量为16,填充因子默认为0.75,扩容时是当前容量翻倍,即2capacity

  • Hashtable基于Map接口和Dictionary类;线程安全,开销比HashMap大,如果多线程访问一个Map对象,使用Hashtable更好;不允许使用null作为key和value;底层基于哈希表结构;初始容量为11,填充因子默认为0.75,扩容时是容量翻倍+1,即2capacity+1

如何保证HashMap线程安全?

使用ConcurrentHashMap,ConcurrentHashMap是线程安全的HashMap,它采取锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap

  • 底层采用分段的数组+链表实现,线程安全
  • 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  • Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,
    ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容。

Java 引用

JDK1.2之后,把对象的引用分为四种状态,即强引用、软引用、弱引用和虚引用。这样的方式,可以更加灵活地控制对象的生命周期。

强引用

String str = new String("123");

只要某个对象与强引用关联,那么JVM在内存不足的情况下,宁愿抛出outOfMemoryError错误,也不会回收此类对象。

如果我们想要JVM回收此类被强引用关联的对象,我们可以显示地将str置为null,那么JVM就会在合适的时间回收此对象。

软引用

java中使用SoftRefence来表示软引用,如果某个对象与软引用关联,那么JVM只会在内存不足的情况下回收该对象。

软引用适合做缓存,在内存足够时,直接通过软引用取值,无需从真实来源中查询数据,可以显著地提升网站性能。当内存不足时,能让JVM进行内存回收,从而删除缓存,这时候只能从真实来源查询数据。

 SoftReference<String> str = new SoftReference<String>(new String("abc"));

弱引用

java中使用WeakReference来表示弱引用。如果某个对象与弱引用关联,那么当JVM在进行垃圾回收时,无论内存是否充足,都会回收此类对象。

弱引用可以在回调函数在防止内存泄露。因为回调函数往往是匿名内部类,一个非静态的内部类会隐式地持有外部类的一个强引用,当JVM在回收外部类的时候,此时回调函数在某个线程里面被回调的时候,JVM就无法回收外部类,造成内存泄漏。在安卓activity内声明一个非静态的内部类时,如果考虑防止内存泄露的话,应当显示地声明此内部类持有外部类的一个弱引用。

WeakReference<String> str = new WeakReference<String>(new String("abc"));

虚引用

java中使用PhantomReference来表示虚引用。虚引用,虚引用,引用就像形同虚设一样,就像某个对象没有引用与之关联一样。若某个对象与虚引用关联,那么在任何时候都可能被JVM回收掉。虚引用不能单独使用,必须配合引用队列一起使用。

当垃圾回收器准备回收一个对象时,如果发现它与虚引用关联,就会在回收它之前,将这个虚引用加入到引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被回收,如果确实要被回收,就可以做一些回收之前的收尾工作。

ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> str = new PhantomReference<String>("abc", queue);

内存泄漏、内存溢出

  • 内存泄漏(Memory Leak)是指程序在申请内存后,无法释放已申请的内存空间。是造成应用程序OOM的主要原因之一。
  • 内存溢出(out of memory)是指程序在申请内存时,没有足够的内存空间供其使用。
  • 内存泄漏会造成内存溢出,内存溢出不一定是内存泄漏造成的

JVM

内存管理

JVM会将它所管理的内存划分为线程私有数据区和线程共享数据区两大类

线程私有数据区包含:

  • 程序计数器:是当前线程所执行的字节码的行号指示器
  • 虚拟机栈:是Java方法执行的内存模型,存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
  • 本地方法栈:是虚拟机使用到的Native方法服务

线程共享数据区包含:

  • Java堆:用于存放几乎所有的对象实例和数组;是垃圾收集器管理的主要区域,也被称做“GC堆”;是Java虚拟机所管理的内存中最大的一块
  • 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

类加载

在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。从而通过牺牲一些性能开销来换取Java程序的高度灵活性。

加载

  1. 通过一个类的全限定名获取定义此类的二进制字节流。
  2. 解析这个二进制流为方法区的运行时数据结构。
  3. 创建一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

由于虚拟机并没有指明二进制字节流要从一个Class文件中获取,准确的说没有规定从哪里获取怎样获取,以下都是可能获取的方式之一:

  • 从本地文件系统加载一个Java class 文件。
  • 通过网络下载一个Java class文件。
  • 从ZIP、JAR、EAR、WAR格式的压缩文件中获取。
  • 运行时生成,JDK提供的动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
  • 由数据库中读取或者其它文件生成,如JSP等等。

连接

验证

确保类加载信息符合JVM规范,并且不会危害虚拟机自身的安全。

准备

为类的静态Field分配内存,并设置初始值。此时设置的初始值时变量的默认值,而不是程序的赋值,例如:

public static int value = 100;

在准备阶段时,value = 0 而不是100。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

该阶段主要是对静态Field进行初始化(真正通过代码来赋值),在Java类中对静态Field指定初始值。

JVM初始化步骤
  1. 假如类没有被加载和连接,则程序先加载并连接该类。
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句。
  4. JVM总是最先初始化java.lang.Object类。
类初始化时机

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName(“com.shengsiyuan.Test”))
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类

Java内部类

  1. 成员内部类
  • 内部类依赖于外部类的实例来创建,此时创建的内部类实例拥有外部类的引用,可以调用外部类的方法(任何访问字符的方法),然而外部类是无法调用内部类的方法的(只能通过内部类的实例来调用)
  • 本质上是两个类Outer.class和Outer.Inner.class
  • 内部类不能有静态方法
  1. 静态内部类
  • 静态类必然是静态内部类
  • 相当于是独立的类,但是不能有抽象方法,同样也不能继承,实例化时可不依赖于外部类
  • 可以直接调用外部类的静态方法和变量
  • 如果外部类的静态成员与内部类的成员名称相同,可通过“类名.静态成员”访问外部类的静态成员;如果外部类的静态成员与内部类的成员名称不相同,则可通过“成员名”直接调用外部类的静态成员。
  1. 方法内部类
  • 局部内部类就像是方法里面的一个局部变量一样,是不能有 public、protected、private 以及 static 修饰符的
  • 只能访问方法中定义的 final 类型的局部变量,因为:当方法被调用运行完毕之后,局部变量就已消亡了。但内部类对象可能还存在,直到没有被引用时才会消亡。此时就会出现一种情况,就是内部类要访问一个不存在的局部变量;使用final修饰符不仅会保持对象的引用不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期.局部内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数;防止被篡改数据,而导致内部类得到的值不一致。
  1. 匿名内部类
  • 匿名内部类是直接使用 new 来生成一个对象的引用;
  • 对于匿名内部类的使用它是存在一个缺陷的,就是它仅能被使用一次,创建匿名内部类时它会立即创建一个该类的实例,
    该类的定义会立即消失,所以匿名内部类是不能够被重复使用;
  • 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口;
  • 匿名内部类中是不能定义构造函数的,匿名内部类中不能存在任何的静态成员变量和静态方法;
  • 匿名内部类中不能存在任何的静态成员变量和静态方法,匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法
  • 匿名内部类初始化:使用构造代码块!利用构造代码块能够达到为匿名内部类创建一个构造器的效果

Java锁

悲观锁和乐观锁

  • 悲观锁认为每次线程操作数据时都会对数据进行修改,所以需要对数据进行上锁防止在访问数据的同时会被其他线程进行修改。
  • 乐观锁认为每次线程操作数据的时候都不会对数据进行修改,所以不会对数据进行加锁,但是在更新数据的时候会判断此时数据是否正在被别的线程进行修改。
  • Java中synchronized和Lock等独占锁就是悲观锁思想的实现。

乐观锁的实现

版本号机制

即每次更新的时候数据都会生成一个新的版本号(一般为数据更新的时间),如果线程进行更新操作的时候发现版本号与读取数据时的版本号不一致时,取消本次更新操作。

CAS算法

CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则就进行不断自旋(无限循环)。

优缺点分析

  • 如果经常进行只读操作,用乐观锁会比较高效。
  • 如果需要经常进行写入操作,乐观锁反而性能降低(因为会反复进行取消操作retry),这个时候用悲观锁更高效。

自旋锁

CAS,如果未能获得线程锁,自旋锁将不断进行自旋操作(这样的操作是消耗CPU资源的)。

序列化和反序列化

发布了63 篇原创文章 · 获赞 73 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/jjwwwww/article/details/99692821