JAVA_基础知识


JAVA 的基础知识

重载和重写的区别。

  1. 重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修 饰符可以不同,发生在编译时。

  2. 重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等 于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

    如果父类是抽象类,那么子类在继承的时候方法参数可以不相同。如果父类不是抽象类,那么子类在继承的时候,方法参数需要相同。

JAVA 面向对象编程的三大特征。

封装 继承 多态

多态

  • 什么是多态?

体现就是重载。重载就是发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修 饰符可以不同,发生在编译时。

Final

  1. final 修饰变量,在被继承的时候,不会被覆盖。
  2. final 修饰方法的时候,在被继承的时候,能被继承,但子类不能被重写。
  3. final 修饰类的时候,不能被继承。

接口和抽象类

  • 接口

接口的关键字是 interface 。它可以看作是一种规范,在使用的过程中只能声明,没有具体的实现。其实现方式需要有不同的子类去实现。

  • 抽象类

抽象类的关键字是 abstract ,同样的是只能声明,不能有具体的实现,其实现方式是通过子类的继承去实现的。

  • 两种的使用

在子类实现接口的过程中,不同的子类去实现同一个接口,会存在一些公共方法的实现,那么对于这些公共的方法,在使用的过程中可以定义为抽象类的形似。

String StringBuffer 和 StringBuilder 和 String 。

  1. 操作少量的数据: 适用String 。

  2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder 。

  3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer。

String 为何是不可变的?

  1. String 的底层就是一个数组,在 JDK1.8 中,String 的底层是被 final 关键字修饰的一个数组。这就保证 String 一旦被创建后就不可改变。但这里的不可改变指的是其对应的引用地址。
  2. 再加上这个引用又被定义为了一个 private 的,那么就更不可能通过继承去改变。同时 String 这类在定义的时候也加了关键字 final 限制了被继承,通过 String 类里也没有定义对应的 Get 和 Set 方法。
  3. 综上所述,String 一旦被初始化就是不可改变的。

== 与 equals

  • 基本数据类型

使用 == 进行比较。

  • 非基本数据类型

重写 equals 方法后,使用 equals 进行比较。(需要注意的是:重写 equals 方法时,需要重写 hashcode 方法,因为会存在两个对象的 Hashcode 方法相同,但 equals 方法不相等的情况。)

如果使用 == 进行比较的话,比较的是两个对象的内存地址。(需要注意的是:String 具有常量池功能,当创建 String 对象的时候,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同,如果有,就会赋值给它。如果没有,就会心创建一个新的 String 对象。)

例如:

// 第一种情况
String a = "a";
String b = "a";
a==b (true)
(String具有常量池功能,当执行String a = "a";的时候,常量池里会重新创建常量”a“。当去执行String b = "a";因为常量池里已经有”a“了,所以b就直接去常量池里取值了,这样a和b的地址也就相同了)。
a.equals(b)true//第二种情况
String s1 = new String("abc");// 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// false,因为一个是堆内存,一个是常量池的内存,故两者是不同。
System.out.println(s1.equals(s2));// true

特例:

// 第一种情况:
String str1 = "str";
幕布 - 极简大纲笔记 | 一键生成思维导图
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象

// 第二种情况
String s1 = new String("abc"); 这句话创建了几个字符串对象?
将创建 12 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

& 和 && 、| 和 || 的区别

  1. && 和 & 都是表示与,区别是 && 只要第一个条件不满足,后面条件就不再判断。而 & 要对所有的 条件都进行判断。

  2. || 和 | 都是表示 “或”,区别是 || 只要满足第一个条件,后面的条件就不再判断,而 | 要对所有的条 件进行判断。

final 关键字

  1. 变量:对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引 用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

  2. 方法:第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因 是效率。

  3. 类: 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final 方法。

ArrayList 与 LinkedList

  1. ArrayList:能够自动增长容量的数组,搜索,读取数据的时候效率较高。 它在内存中是一段连续的地址,在做查询的时候,不需要寻址,所以查询的时候效率会更高。

  2. LinkedList:是一个双向的链表,添加,和删除数据的时候效率较高。它在内存中的存储是分散的,在做查询的时候需要寻址,所以查询的时候效率不是很高。

LinkedList 可以用 For 循环遍历吗?

尽量不要用,LinkedList 的底层是链表,用 For 循环遍历,每访问一个元素都要从头开始访问,然后找到这个元素。

可以使用迭代器遍历。

HashMap 和 HashTable

HashMap

在 JDK1.7 中,它的数据结构是一个数组+链表的数据结构,数据节点是 Entry 节点,在进行数据插入的时候采用的是头插法。在进行扩容的时候节点里的 Resize 方法调用 transfer 方法,然后会把里面的数据造成 Rehash,在下一次 Get 的时候,就会出现一个死循环。

在JDK8中,HashMap的底层是:数组+链表(散列表)+红黑树。同时为了解决JDK1.7 中出现的链表循环,JDK1.8 中使用了尾插法。其中它的初始化容量默认值是16。装载因子默认是0.75。在初始化的时候会先计算扩容的阈值,在插入数据的时候会判断当前的 Size 是否大于这个阈值,如果大于就会创建一个2倍数据大小,扩容到原来的2倍。

当桶上元素有8位,并且散列表的容量大于64的时候,HashMap 会变成红黑树。

HashMap 的插入原理

  1. 判断数组是否为空,为空进行初始化。
  2. 不为空。计算 K 的 Hash 值。通过 (n-1)& Hash 计算存放在数组的下标 Index。
  3. 查看 Table[Index] 是否存在数据,没有数据就构造一个 Node 节点,存放在 Table[Index] 中。
  4. 存在数据。说明发生了 Hash 冲突。判断 Key 是否相等。相等就用新的 Value 替换。
  5. 不相等,判断当前节点类型是不是树型节点,如果是,创建树型节点插入到红黑树。
  6. 如果不是,创建普通 Node 加入到链表中,判断链表长度是否大于 8。大于则转成红黑树。
  7. 插入数据后判断当前节点是否大于阎值,如果大于开始扩容原来数组的 2 倍。

HashMap 为什么每次扩容都是 2 的倍数?

  1. 在向 HashMap 添加数据元素的时候,会使用 (n-1)& Hash 的方法计算相应的元素位置。
  2. 在扩容的时候,会新建一个 Tab,然后遍历旧的 Tab,将旧元素进行 (n-1)& (NewCap - 1) 也就是 (n-1)& Hash 的计算方法。(n 是集合容量,Hash 是添加元素计算而来的 Hash 值)
  3. 其主要是这个 (n-1)& Hash 的计算。
    1. 计算结果比较高效。
    2. 如果 HashMap 的容量刚好是 2 的倍数的时候,(n-1)的二进制与添加元素的 Hash 进行位运算,能够充分散列,减少 Hash 碰撞。

HashMap 解决哈希冲突的方法

  1. 使用链地址法来连接拥有相同 Hash 值的数据。
  2. 使用 2 次扰动函数(Hash 函数)来降低哈希冲突。
  3. 引用红黑树降低遍历时间。

HashMap 是非线程安全的,举几个例子。

  1. HashMap 在插入数据的时候,只保留最后一个线程插入的数据。
  2. HashMap 在修改数据的时候,只保留最后一个线程修改的数据。
  3. HashMap 在扩容的时候,只保留最后一个线程扩容的结果。

HashTable

  1. HashMap 在进行数据写入的时候,允许 Key 和 Value 的值为 Null;HashTable 不允许 Key 和 Value 为Null。
  2. HashMap 是线程不安全的,HashTable 是线程安全的。
  3. ConcurrentHashMap 可以看作是一个线程安全的 HashTable。它提供的是一组和 HashTable功能相同但线程安全的方法。

ConcurrentHashMap

首先 HashTable 是线程安全的,它实现线程安全的方法是对内部每一个方法都使用了 Synchronized 关键字。相当于对每一个方法都添加了对象锁。

ConcurrentHashMap 采用的是分段锁。在 JDK1.8 中 ConcurrentHashMap 底层也是 数组+链表(散列表)+红黑树的数据结构,在加锁的过程中它只会锁住 Entry 所在节点的那个值,在上锁的时候使用的是 CAS + Synchronized 来实现的。

Set

  1. Set底层就是Map 。

  2. HashSet 无序,允许为null,底层是HashMap(散列表+红黑树),非线性同步。

  3. TreeSet 有序,不允许为null,底层是TreeMap(红黑树),非线性同步。

  4. LinkedHashSet 迭代有序,允许为null,底层是HashMap+双向链表,非线性同步。

HashSet 是如何去重的?

HashSet 底层采用 HashMap 存储数据,当向 HashSet 中添加元素的时候,首先计算 hashcode 的值,然后用这个计算而来的 hashcode 对 HashMap 集合大小取余后加 1。((hashcode % HashMap Size)+1)来计算这个元素的存储位置,如果这个位置为空,则将元素添加进去,如果不为空,则用 equals 方法比较元素是否相等,相等就不添加。

什么是哈希?

把任意长度的输入通过散列算法变成固定长度的输出,该输出就是散列值。

特点:

根据同一散列函数计算出的散列值如果不同,那么输入肯定不同。如果计算结果相同,那么输入不一定相同。

哈希冲突

两个不同的输入值,根据同一散列函数计算出相同的散列值。

解决HASH 碰撞的方法

  1. 开发地址法。
  2. 再哈希法。
  3. 链地址法。
  4. 建立一个公共溢出区。

Java 中强引用,软引用,弱引用,虚引用

强引用:最普遍的引用,Object obj = new Object(); 宁愿抛出 OOM 也不愿回收的强引用对象,通过将对象设置成 NULL 来弱化引用,使其被回收,或者等待生命周期结束。

软引用:对象处在有用但非必需的状态,当内存空间不足时,会回收,当内存足够时,会允许它存在。可以用来实现高速缓存。

弱引用:比软引用弱,GC 时一定会被回收,但不一定会被立即回收。

虚引用:不会决定对象的生命周期,任何时候都有可能被垃圾回收器回收。

在程序设计的时候常常使用软引用。因为:软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生

JAVA 多线程中的锁

乐观锁

总是假设情况都是最好的,在使用的时候不会上锁,但更新操作的时候回去做判断,判断是否有其它线程更新了这个数据,适用于读取较多的情况,底层使用:版本号和 CAS 算法实现。

悲观锁

总是假设情况都是最坏的,在使用的时候每次处理数据都会上锁,只是把资源给一个线程使用,其它线程阻塞,用完后把资源转给其它线程。一般适应于写数据较多的情况。

版本号机制

在数据库表中加一个数据版本号 version 字段,表示被修改的次数。当数据被修改时 version 的值会加一。线程更新数据时,会读取这个字段的值并修改。提交更新时,若当前的 version 的值大于数据库当前版本,则更新操作。

CAS 算法

当要读写内存值 V 等于进行比较的值 A 时,CAS 通过原子操作用新值 B 来代替 V。

缺点:

  1. ABA 问题。
  2. 循环时间长,开销大。
  3. 只能保证一个共享变量的原子操作。

JAVA 内存管理

Java 内存模型分为:程序计数器,虚拟机栈,本地方法栈,堆,方法区。

程序计数器

  1. 如果线程执行的是一个 Java 方法,那么计数器中记录的是:正在执行的虚拟机字节码的指令地址。
  2. 如果线程执行的是一个 Native 方法,那么这个计数器的值为空。
  3. 存储空间上不会随程序的执行而改变。

虚拟机栈

  1. Java 方法执行的内存模型。每个方法从被调用到执行完成,都对应着一个入栈和出栈的过程。
  2. 当线程请求的栈深度超过最大值,则抛出:StackOverFlowError 异常。
  3. 当栈动态扩展时,无法申请到足够的内容时,则抛出:OutOfMemoryError 异常。

本地方法栈

  1. 当本地方法栈使用 Native 方法时,虚拟机会把本地方法栈和虚拟机栈合二为一。

  1. 所有线程共享,几乎所有对象的实例都在这里分配内容。
  2. 主要职责是负责垃圾收集管理区域。

永久代

在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。

方法区

  1. 存放已被加载的信息常量,在垃圾回收时负责常量池的回收和对类的卸载。

类的加载

类加载检查 ——> 分配内存 ——> 初始化零值 ——> 设置对象头 ——> 执行 init 方法。

类加载检查:

虚拟机遇到 New 指令 ——> 指令参数在常量池中定位符号引用 ——> 符号引用代表类是否被加载过,解析和初始化 ——> (没有) 执行加载。

分配内存:

  • 分配方法:
  1. 指针碰撞 ——> 堆内存规整 ——> 标记整理。
  2. 空闲列表 ——> 堆内存不规则 ——> 标记清理。
  • 内存分配并发问题,两种处理方法:
  1. CAS + 失败重试。
  2. TLAB。

JAVA 垃圾回收

分代垃圾收集算法,将 Java 内存区域分为新生代和老年代。

新生代又分为:E 空间,F 空间,T 空间。

分配方法:

首先在 E 区分配,在第一次垃圾回收之后,如果还存在则会进入 F 或 T。并且对象年龄加 1 。当年龄增加到一定程度,默认是 15 ,会进入到老年代。

经过这次收集,E 和 F 会被清空,然后 F 和 T 交换角色。直到 T 已被填满将对象移入老年代。

几个特点:

  1. 对象优先在 E 上分配。
  2. 大对象直接进入老年代。
  3. 长期存活的对象将进入老年代。

使用到的算法:

  1. 标记-清除法:先标记不需要回收的对象,标记完成后,统一回收没被标记的对象。(引发的问题:)效率问题,空间问题。
  2. 复制算法:(解决效率问题)将内存分为大小两块,每次使用其中一块,当一块使用完成,将存活的对象复制到另一块,然后在把使用的空间清理。(这样就使每次的内存回收都是对内存区间的一半进行回收。 )
  3. 标记-整理算法:(解决空间问题)先标记不需要回收的对象,标记完成,将所有存活的对象向一端移动,清理掉边界以外的内存。

收集器

  1. 串行收集器:它是一个单线程的收集器。这就意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,然后它在进行垃圾收集工作的时候必须暂停其他所有的工作线程,直到它收集结束。 (新生代采用复制算法,老年代采用标记-整理算法 。)

  2. ParNew 收集器:可以看作是串行收集器的多线程版本。(新生代采用复制算法,老年代采用标记-整理算法 。)

    • 并行 :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    • 并发:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。
  3. CMS 收集器:CMS收集器是一种 “标记-清除”算法实现的,一款真正意义上的并发收集器。

工作过程主要分为:

  • 初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;
  • 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫。

明显缺点:

  • 对CPU资源敏感;
  • 无法处理浮动垃圾;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
  1. G1 收集器:执行步骤主要包括:初始标记,并发标记,最终标记,筛选回收。

CMS 收集器和 G1 收集器

  • 使用范围不一样

    1. CMS 收集器是老年代收集器,可以配合新生代的 串行和并行 收集器一起使用。
    2. G1 收集器收集范围是老年代和新生代,不需要结合其他收集器使用。
  • STW 的时间

    1. CMS 收集器以最小停顿时间为目标收集器。
    2. G1 收集器可预测垃圾回收的停顿时间。
  • 垃圾碎片

    1. CMS 收集器是使用“标记-清除”算法进行垃圾回收,容易产生内存碎片。
    2. G1 收集器使用“标记-整理”算法,进行空间整合,降低了内存空间碎片。
  • 垃圾回收过程不一样

    1. CMS 收集器:初始化标记 —> 并发标记 —> 重新标记 —> 并发清楚。
    2. G1 收集器:初始化标记 —> 并发标记 —> 最终标记 —> 筛选回收。

如何判断对象已经死亡?

  1. 引用计数法 ——> 会存在循环引用的问题。
  2. 可达性分析算法 —— > 通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
  • 不可达的对象并非非死不可

    真正宣告这个对象的死亡,至少要经历两次标记过程。不可达的对象第一次被标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过了。虚拟机将这两种情况视为没有必要执行。

    被判定为需要执行的对象将会被放在一个队列中进行二次标记,除非这个对象与引用链上的任何一个对象关联否则就会被回收。

如何判断一个常量是废弃常量?

当前没有任何 String 引用该字符串常量。

如何判断一个类是无用类?

满足一下三个条件:

  1. 该类所有的实例都已经被回收。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 没有任何引用实例。

文章作者: L Q
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 L Q !
  目录