JAVA基础

161 阅读20分钟

1、重载和重写的区别

重载:

  • 发生在同一个类中
  • 方法名必须相同
  • 参数列表不同
  • 方法返回值和访问修饰符可以不同
  • 发生在编译时 重写:
  • 发生在父子类中
  • 方法名、参数列表必须相同
  • 返回值范围小于等于父类
  • 抛出的异常范围小于等于父类
  • 访问修饰符范围大于等于父类
  • 如果父类方法访问修饰符为private则子类就不能重写该方法

2、String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?

可变性:

  • String类中使用final关键字字符数组保存字符串,private final char value[],所以String对象是不可变的。
  • 而StringBuilder与StringBuffer都继承自AbstractStringBuilder 类,在AbstractStringBuilder中也是使用字符数组保存字符串char[] value 但是没有用final关键字修饰,所以这两种对象都是可变的。 线程安全性:
  • String 中的对象是不可变的,也就可以理解为常量,线程安全。
  • StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
  • StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 性能
  • 每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。
  • StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。
  • 相同情况下使用StirngBuilder相比使用StringBuffer仅能获得10%~15%左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  • 操作少量的数据 => 使用String
  • 单线程操作字符串缓冲区下操作大量数据 => 使用StringBuilder
  • 多线程操作字符串缓冲区下操作大量数据 => 使用StringBuffer

3、自动装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

4、==与equals

  • ==: 它的作用是判断两个对象的地址是不是相等。即: 判断两个对象是不是同一个对象。
    1. 基本数据类型==比较的是值。
    2. 引用数据类型==比较的是内存地址。
  • equals(): 它的作用也是判断两个对象是否相等。但它一般有两种使用情况,如下:
    1. 类没有覆盖 equals() 方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
    2. 类覆盖了equals() 方法。一般,我们都覆盖 equals()方法来比较两个对象的内容;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

5、final关键字

final关键字主要用在三个地方:变量、方法、类。

  1. final变量
    • 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
    • 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  2. final类,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。
  3. final方法,使用final方法有两个原因:
    • 第一个原因是把方法锁定,以防任何继承类修改它的含义;
    • 第二个原因是效率。

6、Java中的异常处理

在Java中,所有的异常都有一个共同的祖先java.lang包中的Throwable类
Throwable:有两个重要的子类:Exception(异常)Error(错误),二者都是Java异常处理的重要子类,各自都包含大量子类。

7、error 和 exception 的区别?

  • Error类和Exception类的父类都是Throwable类。
  • Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
  • Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。Exception 类又分为运行时异常(Runtime Exception)和受检查的异常(CheckedException ),运行时异常。

8、接口和抽象类的区别是什么?

  1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法。
  2. 接口中的实例变量默认是final类型的,而抽象类中则不一定。
  3. 一个类可以实现多个接口,但最多只能实现一个抽象类。
  4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定。
  5. 接口不能用new实例化,但可以声明,但是必须引用一个实现该接口的对象。从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 备注: 在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。

9、什么是单例模式?有几种?

定义:某个类的实例在多线程环境下只会被创建一次出来。

  1. 饿汉式: 线程安全,一开始就初始化。
  2. 懒汉式: 非线程安全,延迟初始化。
  3. 双检锁: 线程安全,延迟初始化。

10、BIO、NIO、AIO 有什么区别?

  • BIO:Block IO同步阻塞式IO,就是我们平常使用的传统IO,它的特点是模式简单使用方便,并发处理能力低。
  • NIO:New IO同步非阻塞IO,是传统IO的升级,客户端和服务器端通过Channel(通道)通讯,实现了多路复用。
  • AIO:Asynchronous IO是NIO的升级,也叫NIO2,实现了异步非堵塞IO,异步IO的操作基于事件和回调机制。

11、Java 集合体系有什么?

集合类存放于Java.util包中,主要有3种:set、list、map。

  1. List
  • ArrayList:底层结构是数组,查询快、增删慢,线程不安全。
  • LinkedList:底层结构是链表,增删快、查询慢。
  • TreeList:树型结构,保证增删复杂度都是O(log n),增删性能远高于ArrayList和LinkedList,但是稍微占用内存
  1. Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复。
  • HashSet:底层数据结构是哈希表, 是线程不安全的, 数据不同步。
    • HashSet 是如何保证元素唯一性的呢?
      • 是通过元素的两个方法,hashCode和equals来完成。如果元素的HashCode值相同,才会判断equals是否为true。如果元素的hashcode值不同,不会调用equals。注意,对于判断元素是否存在,以及删除等操作,依赖的方法是元素的hashcode和equals方法。
  • TreeSet:底层数据结构是二叉树,存放有序:TreeSet线程不安全可以对Set集合中的元素进行排序。通过compareTo或者compare方法来保证元素的唯一性。
  1. Map:该集合存储key-value键值对,而且要保证键的唯一性。
  • HashMap:基于hash表的Map接口实现,非线程安全,高效,支持null值和null键;
  • HashTable:线程安全,低效,不支持null值和null键;
  • LinkedHashMap:是HashMap的一个子类,保存了记录的插入顺序;
  • TreeMap,底层数据结构是二叉树,线程不同步,可以用于给map集合中的键进行排序,默认是升序。

12、HashMap 底层实现原理

  • HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值。因而具有很快的访问速度,但是遍历顺序却不确定的。
  • HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
  • HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。 jdk1.8之前数组+链表(拉链法),jdk1.8之后是数组+链表+红黑树。

13、 ConcurrentHashMap的特点

线程安全

  • 整个ConcurrentHashMap由一个个Segment分段锁组成。
  • Segment继承ReentrantLock。 简单理解就是,ConcurrentHashMap是一个Segment数组,Segment通过继承ReentrantLock来进行加锁,所以每次需要加锁的操作锁住的是一个segment,这样只要保证每个Segment是线程安全的,也就实现了全局的线程安全。

并行度(默认 16)

  • ConcurrentHashMap有16个Segments,所以理论上,最多可以同时支持16个线程并发写,只要它们的操作分别分布在不同的Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。

Java8实现

  • Java8对ConcurrentHashMap进行了比较大的改动,Java8也引入了红黑树

14、HashMap,HashTable,ConcurrentHashMap 之间的区别

性能:ConcurrentHashMap(线程安全) > HashMap > HashTable(线程安全)

  • HashMap和HashTable区别:
    • HashMap是非线程安全的,HashTable是线程安全的。
    • HashMap的键和值都允许有null值存在,而HashTable则不行。
    • 因为线程安全的问题,HashMap效率比HashTable的要高。
    • Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。一般现在不建议用HashTable, ① 是HashTable是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下,现在也有同步的ConcurrentHashMap替代,没有必要因为是多线程而用HashTable。
  • HashTable和ConcurrentHashMap区别:
    • HashTable使用的是Synchronized关键字修饰,ConcurrentHashMap是使用了segment分段锁技术来保证线程安全的。
    • Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

15、线程和进程

  • 线程: 是进程的一个实体,是cpu调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
  • 进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。是操作系统进行资源分配和调度的一个独立单位。

16、进程与线程的区别

进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

17、创建线程有几种方式

  1. 继承Thread类,重写run方法

  2. 实现Runnable接口,重写run方法

    • 实现Runnable接口比继承Thread类所具有的优势:
      1. 适合多个相同的程序代码的线程去共享同一个资源。例:声明成员变量
      2. 可以避免java中的单继承的局限性。
      3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和数据独立。
      4. ThreadPoolExecutor线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
  3. 实现Callable接口,与Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

    • call()方法可以有返回值
    • call()方法可以声明抛出异常
  4. 线程池 线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。

  • 合理利用线程池能够带来三个好处
    1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  1. 匿名内部类

18、常用的线程池有哪些

  • newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
  • newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

19、线程的基本方法

  1. wait 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用wait()方法后,会释放对象的锁。因此,wait方法一般用在同步方法或同步代码块中。
  2. sleep 导致当前线程休眠,与wait方法不同的是sleep不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会 导致当前线程进入WATING状态.
  3. yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下, 优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。
  4. interrupt 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的 一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)
  5. Join 等待其他线程终止,在当前线程中调用一个线程的join() 方法, 则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待cpu
  6. notify Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个wait()方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有notifyAll() ,唤醒再次监视器上等待的所有线程。

20、 wait与sleep区别

  • sleep()方法是属于Thread类中的。而wait()方法,则是属于Object类。
  • 在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁。 总结来说sleep适合定时唤醒,通常被用于暂停执行。wait适合条件唤醒。通常被用于线程间交互。

21、死锁产生的条件以及如何避免?

死锁产生的四个必要条件

  • 互斥:一个资源每次只能被一个进程使用(资源独立)。
  • 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放(不释放锁)。
  • 不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺(抢夺资源)。
  • 循环等待:若干进程之间形成一种头尾相接的循环等待的资源关闭(死循环)。

避免死锁:

  • 破坏”互斥”条件:系统里取消互斥、若资源一般不被一个进程独占使用,那么死锁是肯定不会发生的,但一般“互斥”条件是无法破坏的,因此,在死锁预防里主要是破坏其他三个必要条件,而不去涉及破坏“互斥”条件。
  • 破坏“请求和保持”条件
    1. 所有的进程在开始运行之前,必须一次性的申请其在整个运行过程 各种所需要的全部资源。
      • 优点:简单易实施且安全。
      • 缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源 也不会得到利用,严重降低了资源的利用率,造成资源浪费。
    2. 该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到,已经使用完毕的资源,然后再去请求新的资源。这样的话资源的利用率会得到提高,也会减少进程的饥饿问题。
  • 破坏“不剥夺”条件:当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂的释放或者说被抢占了。
  • 破坏“循环等待”条件:可以通过定义资源类型的线性顺序来预防,可以将每个资源编号,当一个进程占有编号为 i 的资源时,那么它下一次申请资源只能申请编号大于 i 的资源。

22、JVM 是什么?JVM 的基本结构

虚拟机,一种能够运行java字节码的虚拟机。

  1. 类加载子系统
  2. 加载 .class 文件到内存。
  3. 内存结构
  4. 运行时的数据区。
  5. 执行引擎
  6. 执行内存中的.class,输出执行结果(包含 GC:垃圾收集器)。
  7. 本地方法的接口。
  8. 本地方法库。

23、JVM内存结构

JDK1.7

image.png

  • 程序计数器 就是一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
  • Java虚拟机栈 Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。
  • 本地方法栈 和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行 native方法服务。登记native方法,在Execution Engine执行时加载本地方法库。
  • Java虚拟机管理的最大的一块内存区域,Java堆是线程共享的,用于存放对象实例。也就是说对象的出生和回收都是在这个区域进行的。
  • 方法区 线程共享,用于存储已经被虚拟机加载的类信息、常量、静态变量等数据。

JDK1.8

image.png

JDK1.8与1.7最大的区别是在1.8中方法区是由元空间(元数据区)来实现。常量池移到堆中。

24、类加载机制

  • 全盘负责委托机制 当A类中引用B类,那么除非特别指定B类的类加载器,否则就直接使用加载A类的类加载器加载B类。
  • 双亲委派机制 指先委托父类加载器寻找目标类,在找不到的情况下再在自己的路径中查找并载入目标类。

25、GC

内存空间是有限的,那么在程序运行时如何及时的把不再使用的对象清除将内存释放出来,这就是 GC 要做的事。
GC的区域在哪里

  • JVM中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。因此,我们的内存垃圾回收主要集中于Java堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

26、GC常用算法

  • 标记-清除算法 为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。
  • 标记-压缩算法(标记-整理) 标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。
  • 复制算法 该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
  • 分代收集算法 现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理或者标记-清除。