Java 基础
面向过程、面向对象
面向过程:以步骤划分问题
面向对象:以功能划分问题
Java 和 C++ 的区别
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承
- Java 有自动内存管理机制,不需要程序员手动释放无用内存
多态、封装、继承
封装:封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法
继承:是为了重用父类代码
多态:一个对象具备多种形态,也可以理解为事物存在的多种体现形态(父类的引用类型变量指向了子类对象,或者是接口的引用类型变量指向了接口实现类的对象)
多重继承、多继承、接口、抽象类
-
-
多继承就是一个类继承了多个类(Java 中不允许)
-
接口
- Java 中可以实现多个接口
- 接口中只能做方法声明(Java 8 开始接口方法可以有默认实现)
- 接口中的变量只能 public static。(假设有两个接口A和B,而类C实现了接口A和B。假设,此时,A和B中都有一个变量N,如果N不是static类型的,那么在C中该如何区分N到底是A的还是B的呢?而,如果是static类型的,我们可以通过A.N和B.N来区别调用A或者B中的成员变量N)
-
抽象类
- 不能实例化对象
- 抽象类中可以实现方法(可以有非抽象方法)
重写(Override)、重载(Overload)
-
重写(子类重写父类方法)返回值和形参都不能改变(同名同参)
-
重载是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同(同名不同参,返回值无关)
- 无法以返回值类型作为重载函数的区分标准
基本类型和引用类型区别
- 赋值方法不同,基本类型直接赋值,引用类型通过 new 创建对象,然后再把对象赋予相应的变量
- 比较方面的不同,== 号的比较:引用类型比较的是引用地址,基本类型比较的是值
- 在数据做为参数传递的时候,基本数据类型是值传递,而引用数据类型是引用传递(地址传递)
- 分别放在 JVM 的哪里?
基本数据类型在被创建时,在栈上给其划分一块内存,将数值直接存储在栈上
引用数据类型在被创建时,首先要在栈上给其引用(句柄)分配一块内存,而对象的具体信息都存储在堆内存上,然后由栈上面的引用指向堆中对象的地址
final
- final 修饰的类叫最终类,该类不能被继承
- final 修饰的方法不能被重写
- final 修饰的变量不可更改。如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象
finally 一般作用在 try-catch 代码块中,表示不管是否出现异常,该代码块都会执行
String StringBuffer 和 StringBuilder
可变性
String 不可变
如何实现不可变
private final byte[] value
。final 修饰 value,所以 value 的引用不能改变- 同时 String 并没有提供接口来改变 value 的值,所以 value 的值我们从 String 外部获取不到,也改变不了
这才是String 才是不可变的真正原因,并不仅仅是使用 final 修饰了 value 数据
为什么设置为不可变
- 为了实现字符串常量池(只有当字符是不可变的,字符串池才有可能实现)
- 为了线程安全(字符串自己便是线程安全的)
- 为了保证同一个对象调用 hashCode() 都产生相同的值,String 设置为不可变可以对这个条件有很好的支持,这也是 Map 类的 key 使用 String 的原因
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的
性能
StringBuilder > StringBuffer > String
对于三者使用的总结:
- 操作少量的数据: 适用 String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
hashCode()
hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等
HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法
缓存池和字符串常量池
缓存池
new Integer(123)
与 Integer.valueOf(123)
的区别在于:
new Integer(123)
每次都会新建一个对象Integer.valueOf(123)
会使用缓存池中的对象,多次调用会取得同一个对象的引用
valueOf()
判断值是否在缓存池中,如果在的话就直接返回缓存池的内容,不在就新建一个
字符串常量池(String Pool)
String Pool 保存着所有字符串字面量(literal strings)
- new String(“abc”) 会创建两个对象(前提是 String Pool 中还没有 “abc” 字符串对象)
“abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量; 而使用 new 的方式会在堆中创建一个字符串对象。
反射(不太懂)
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁
举例:当我们在使用 IDE 时,输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。反射最重要的用途就是开发各种通用框架
缺点:效率低
异常
- 运行时异常将由 Java 运行时系统自动抛出,允许应用程序忽略运行时异常
- 单个的代码段可能引起多个异常,我们需要定义两个或者更多的 catch 子句
关键字
- try:用于监听。将要被监听的代码放在 try 语句块之内,当try语句块内发生异常时,异常就被抛出
- catch:用于捕获异常。用来捕获 try 语句块中发生的异常
- finally:finally 语句块总是会被执行。它主要用于回收在 try 块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally 块,执行完成之后,才会回来执行 try 或 catch 块中的return 或者 throw 语句,如果 finally 中使用了 return 或 throw 等终止方法的语句,则就不会跳回执行,直接停止
- throw:用于抛出异常
- throws:用在方法签名中,用于声明该方法可能抛出的异常
泛型
泛型类、泛型方法、泛型接口。数据类型参数化
泛型的好处是在编译的时候检查类型安全
泛型只在编译阶段有效
泛型擦除
注解
Java 注解可以在类、字段变量、方法、接口等位置进行一个特殊的标记,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。也可以,代码生成、数据校验、资源整合等工作做铺垫
@target \ @retention 是元注解。元注解是注解的注解
为什么 main 方法是静态的
如果 main 方法不声明为静态的,JVM 就必须创建 main 类的实例,因为构造器可以被重载,JVM 就没法确定调用哪个 main 方法
Java 容器
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表
Collection
Set(元素唯一)
- HashSet(无序): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet(遍历序和插入序一致): 存储结构是一个双向链表
- TreeSet(会对元素进行排序): 红黑树(自平衡的排序二叉树)
带 "Hash" 的查找时间都是 ,TreeSet 则为
List(遍历序和插入序一致)
- ArrayList:基于动态数组实现,支持随机访问
- Vector:和 ArrayList 类似,但它是线程安全的
- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列
Queue
- LinkedList:可以用它来实现双向队列
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列
阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中
Map
- HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,保存了记录的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
- Hashtable:数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap:红黑树(自平衡的排序二叉树)
Collection 与 Collections 的区别
- Collection 是一个接口,它提供了对集合对象进行基本操作的通用接口方法
- Collections 是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作
Collections.sort()
Collections.sort 方法底层就是调用的 Arrays.sort 方法,而 Arrays.sort 使用了两种排序方法,快速排序和优化的归并排序。
快速排序主要是对那些基本类型数据(int, short, long等)排序, 而归并排序用于对 Object 类型进行排序
ArrayList 与 Vector 区别
- Vector 线程安全。同步操作比较耗时
- Arraylist 不是同步的
HashMap 和 Hashtable 的区别
- 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
synchronized
修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!) - 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它
- 对Null key 和Null value的支持:在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException,HashMap 允许 null
- 初始容量大小和每次扩充容量大小的不同:①创建时如果不指定容量初始值,HashTable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 HashTable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的
tableSizeFor()
方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方 - 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制
HashMap 和 HashSet区别
HashSet 底层是基于 HashMap 实现的
HashMap | HashSet |
---|---|
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用 put() 向map中添加元素 |
调用 add() 方法向Set中添加元素 |
HashMap 使用键计算 Hashcode | HashSet 使用成员对象来计算 hashcode |
如何检查重复:先检查 hashcode,如果没有相同的值,那么无重复元素,可插入;如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同
HashMap 的底层实现
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度,取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突
JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
为什么 HashMap 不用 B+ 或其他平衡二叉树?
B+ 树适用于数据量较大的场景,数据量大就要存在磁盘上,B+ 树一个结点有多个关键字,可以减少磁盘 IO
ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用分段的数组+链表实现,JDK1.8 采用的数据结构跟 HashMap 的结构一样,数组+链表/红黑树。HashTable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用数组+链表的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- **实现线程安全的方式(重要)
对比图:
JDK 1.7:
JDK 1.8:
ConcurrentHashMap 线程安全的实现方式
JDK1.7(上面有示意图)
首先将数据分为一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁
JDK1.8 (上面有示意图)
ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑树
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升N倍
Java 并发
并发与并行
- 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行)
- 并行: 单位时间内,多个任务同时执行
线程的运行状态
为什么要使用多线程呢?
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能
使用多线程可能带来什么问题?
内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题
什么是上下文切换?
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。这可能是操作系统中时间消耗最大的操作
sleep() 方法和 wait() 方法区别和共同点?
- 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了对象锁
- 两者都可以暂停线程的执行
- wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒
⭐为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行
synchronized 关键字
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
使用方式:synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!