一、面向对象基础
①.简述面向对象的三大特征 ★
从 是什么?好处?实现?三个方面展开叙述
封装、继承、多态
1.封装:概念:
是将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。
好处 :
①便于修改,增强了代码的可维护性。
②隐藏了实现细节,还要对外提供可以访问的方式。便于调用者的使用。
③提高了安全性。
实现:
使用private修饰符实现属性私有化,并提供setter和getter方法
2.继承:
概念:
继承是把多个类中相同的内容给提取出来定义到一个类中,这个类就叫做父类,又称超类或基类,而多个类就是子类
好处:
①提高了代码的复用度,减少了代码的重复,提高软件开发效率
②继承的出现让类与类之间产生了关系,提供了多态的前提
实现:
通过关键字extends可以声明一个类是从另外一个类继承而来
3.多态概念:
接口的多种不同实现方式,父类型引用指向子类型对象。
好处:①简化了代码
②提高了维护性和扩展性
缺点:①通过父类引用操作子类对象时,只能使用父类中已有的方法,不能操作子类特有的方法
实现:
①存在继承关系
②存在方法重写
③父类型引用指向子类型对象
②.接口和抽象类的区别 ★
接口 | 抽象类 | |
---|---|---|
构造方法 | 无 | 有,用于子类实例化使用 |
抽象方法/具体方法 | 只有抽象方法 | 可以有抽象方法和具体方法 |
修饰符 | 只有private | 四种修饰符都有(public 、缺省、protected、private) |
成员变量 | 只能是常量(省略 public static final ) | 可以是变量,也可以是常量 |
具体方法:静态方法、默认方法
1.为什么要引入静态方法?
解决接口的修改与现有的实现不兼容问题。因为以前要加入抽象方法时,抽象类必须要实现方法,加入默认方法后,后续实现的接口,对他们没有多大影响。
2.为什么要引入默认方法?
引进静态方法之后,可以通过接口名来调用,不用通过new实现类了。
③.重写和重载的区别 ★
- 重写
1.发生在子类中
2.方法名、形参列表、返回类型都相同,权限修饰符的范围要大于父类方法,声明异常范围要小于父类方法
3.子类和父类再同一个包中,子类可以重写父类所有的方法。除了声明为private和final的方法和构造方法
- 重载
1.发生在同一类中
2.方法名相同,参数列表、返回类型、权限修饰符可以不同
④基本类型和封装类型
包装类
直接能够将简单类型的变量。在执行变量类型相互转换时,会使用大量包装类
基本类型 VS 包装类型
基本类型 包装类型 声明方式不同 不需要new来创建 需要通过new的关键字的方式来创建 存储方式及位置不同 是直接存储变量的值保存在堆栈中能高效的存取 需要通过应用指向实例,具体的实例保存在堆中 初始值不同 视具体值而定 初始值为null 使用方式不同 eg:集合类合作使用时只能使用包装类型
基本类型和包装类型相互转换基本类型--->包装类型: 通过new Integer(6);
包装类型--->基本类型: 通过parseInt();
⑤内部类
当遇到比较复杂的问题,想创建一个类来辅助我们解决问题,但又不希望这个类是公共可用的,于是就有了局部内部类
1.匿名内部类
实现方式:
①继承抽象类,重写方法
②实现抽象类,重写方法
abstract class 匿名内部类_抽象类{ abstract void eat(); } abstract interface 匿名内部类_接口{ void eat(); } // 继承抽象类 class 继承匿名内部类_抽象类{ public static void main(String[] args) { 匿名内部类_抽象类 匿名内部类_抽象类 = new 匿名内部类_抽象类() { @Override void eat() { System.out.println("妈妈我想吃考红薯"); } }; 匿名内部类_抽象类.eat(); } } //实现接口 class 实现匿名内部类_接口{ public static void main(String[] args) { 匿名内部类_接口 匿名内部类_接口 = new 匿名内部类_接口() { @Override public void eat() { System.out.println("吃,吃大个的"); } }; 匿名内部类_接口.eat(); } }
特点:
①匿名内部类必须继承一个抽象类或者实现一个接口
②匿名内部类不能定义任何静态类成员和静态方法
③当所在的方法的形参需要被匿名内部类使用时,必须声明为final
④匿名内部类不能是抽象的,它必须要实现继承的类或者实现接口中所有的抽象方法
优点:①匿名内部类不能是抽象的,必须要实现继承的类或者实现接口中所有抽象方法
②匿名内部类可以很方便的定义回调
③内部类有效实现了"多重继承" ,优化了java单继承的缺陷
局部内部类:
定义在方法中内部类,就是局部内部类。
定义在实例方法中局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类类只能访问外部类的静态变量和方法。
class 局部内部类{ void InnerClass(){ class Inner内部类{ void say (){ System.out.println("我是局部内部类"); } } // 局部内部类实例化 ,只能在方法内局部使用 Inner内部类 inner内部类 = new Inner内部类(); inner内部类.say(); } }
使用场景:
当遇到比较复杂的问题,想创建一个类来辅助我们解决问题,但又不希望这个类是公共可用的,于是就有了局部内部类。
静态内部类
定义在类中的静态类,就是静态内部类
class 静态内部类 { static class 静态的类 { void say() { System.out.println("我是静态内部类"); } } public static void main(String[] args) { //调用方式 静态内部类.静态的类 实例静态内部类 = new 静态内部类.静态的类(); 实例静态内部类.say(); } }
优点:
可以访问外部类所有的静态变量,而不可以访问外部类的非静态变量
非静态内部类
定义在类中的非静态类,就是非静态内部类
class 非静态内部类 { class 非静态的类 { void say() { System.out.println("我是非静态内部类"); } } public static void main(String[] args) { // 调用方式 非静态内部类 非静态内部类 = new 非静态内部类(); 非静态内部类.非静态的类 非静态的类 = 非静态内部类.new 非静态的类(); 非静态的类.say(); } }
特点:可以访问外部类所有的变量和方法,包括静态和非静态
二、常用类
1.字符串
①简述String常用的方法 ★
concat()
length()
substring()
valueOf()
Equals()比较;
charAt(int index)返回指定索引的char值;
concat(String str)将指定的字符串,此字符串的末尾;
contains(CharSequence s)包含制定字符返回true;
indexOf(String str) 返回指数在这个字符串指定的子字符串中第一个出现的;
length()返回字符串长度;
split(String regex)根据指定字符切割字符串,返回数组;
substring(int beginIndex int endIndex),返回指定索引值之间的字符串;
trim()返回一个字符串,任何前导和尾随空格删除;
valueOf(int i) 返回的 int参数的字符串表示形式;
replace(String oldChar, String newChar) 返回从字符串中替换所有出现在 newChar oldChar结果字符串。
②简述String、StringBuffer、StringBuilder各自的特点及相关类图 ★
String | StringBuffer | StringBuilder | |
---|---|---|---|
概念 | String的值是不可变的,这会导致每次对String的操作都会产生新的String对象。 不仅效率低下,而且会浪费大量的内存空间。 | StringBuffer是可变类,任何对它指向的字符串的操作都不会产生新的对象。 每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会非配新的容量。当字符串大小超过容量时,会自动增加容量。 | 可变类,效率更高 |
是否安全 | 线程安全 String是final修饰的类,是不可变的,所以是线程安全。 | 线程安全 | 线程不安全 |
使用场景 | 操作少量字符串 | 多线程操作字符串 | 单线程操纵字符串 |
③== 和 equals的区别 ★
- ==
1.如果比较的对象时基本类型,则比较数值是否相等
2.如果比较的是应用数据类型,则比较的对象是内存地址是否相等
- equals
用来比较两个对象的内容是否相等
④String 类能被继承吗,为什么 ★
String 是一个 final 修饰的类,被final 修饰类不可以被继承
原因:
1. 效率性,String 类作为最常用的类之一,禁止被继承和重写,可以提高效率
2.安全性,String 类中有很多调用底层✁本地方法,调用了操作系统的API,如果方法可以重写,可能被植入恶意代码,破坏程序
三、异常
①final、finally,finalize的区别 ★
- final:
可以修饰全局变量,表示此变量必须赋初值,且后期不可更改。
可以修饰局部变量,表示该局部变量可以不赋初值,一旦赋值后不可更改。
可以修饰类,表示该类不可被继承
可以修饰方法,表示该方法,不可以被重写
- finally:
通常与try-catch联用,声明在finally的语句代码块一定会被执行。
一般用于资源释放。
- finalize:
是垃圾回收机制中使用到的方法。
②异常的分类有哪些?
编译时异常:
一般是由于代码逻辑不严谨造成的,可以通过修改代码解决异常;
eg:
①NoSuchMethodException: 方法未找到异常
②IOException:输入输出异常
③FileNotFoundException: 文件未找到异常
④NumberFormatException: 字符串转换为数字异常
运行时异常:
eg:
①NullPointerException:空指针异常
②ArrayOutOfBoundsException:数组索引越界异常
③ClassNotFoundException:类未找到异常
④ClassCastException:类强制转换异常
③throw和throws的区别
throw | throws | |
---|---|---|
位置 | 声明在方法内部中,后面跟的是异常对象名 | 声明在类上、方法上,后面跟的是异常类名 |
用法 | 只能抛出一个异常,并把异常交给方法来处理 | 可以抛出多个异常,并抛给方法或类的调用者 |
抛出异常的可能性 | 声明异常后,一定会抛出异常 | 表示出现异常的可能性,不一定会抛出异常 |
四、集合框架
①集合框架都包含哪些,并简述之间的区别
集合框架:Collection、Map
(1)Collection(存放单值、有序)
(2)Map(存放键值对、键不允许重复)。
Collection:List、Set
List接口:(存放单值、有序、可重复、可通过索引查找元素)
(1)ArrayList(动态数组、查询速度快)
(2)LinkedList(链表结构、插入、删除操作快、访问慢)
(3)Vector·(与ArrayList相比,线程安全,速度慢)
Set接口:
(1)HashSet(散列表结构、无 序、不可重复)、
(2)LinkedHashSet(继承HashSet,内部使用LinkHashMap,遍历和插入顺序一致)、
(3)TreeSet(默认自然排序)
Map:
(1)HashMap(散列表、存键值对、不是线程安全的、允许存放null)、
(2)LinkedHashMap(将HashMap和双向链表合二为一,保持插入和访问顺序)、
(3)TreeMap(二叉树结构、对健自然排序)、
(4)HashTable(散列表结构、线程安全、键和值都不为空)、
(5)ConcurrentHashMap(JDK1.5之后替代HashTable)
②List set Map之间的区别?
- 先总
Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接
口。我们比较常用的是Set、List,Map接口不是collection的子接口。
-
后分
-
Set、List和Map的区别
不同 List Set Map 存储特点 单列,存取有序,元素有索引、元素可以重复、允许空值 单列,存取无序,元素无下标、元素不可以重复**,只允许一个null值元素**,保证元素的唯一性 双列,键不可重复,只能一个空键 遍历方式 for、迭代器 迭代器 迭代器 对数据操作 可以动态增长,查找效率高,插入删除元素效率低,会引起其他位置改变。 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 Map中不能由get()方法来判断Map中是否存在某个键,而应该用containsKey()方法来判断
-
HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
-
LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
-
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
-
TreeMap: 红黑树(自平衡的排序二叉树)
-
-
③怎么确保一个集合不被修改
集合工具类中有unModifiablexxx()的方法,可以实现集合不被修改。
eg: static List unmodifiableList(List<? extends T> list)
public static void main(String[] args) { ArrayList<Object> list = new ArrayList<>(); list.add("111"); List<Object> objects = Collections.unmodifiableList(list); for (int i = 0; i < objects.size(); i++) { Object o = objects.get(i); System.out.println(o); } objects.add(111); //此处运行会报错 }
1.List接口
①List常用哪些方法有哪些?
boolean add(E e): 向列表的尾部添加指定的元素。
void add(int index, E element): 向列表的指定位置插入指定元素。
boolean addAll(Collection<? extends E> c): 添加指定collection中的所有元素到此列表的结尾。
void clear(): 清空列表中元素。
E remove(int index): 删除指定位置的元素,并返回删除的元素。
boolean remove(Object obj): 删除列表中第一次出现的指定对象,如果存在返回true。
E set(int index, E element): 替换列表中指定位置的元素,返回被替换的元素。
int size(): 获取列表中的元素数量,如果此列表元素个数大于Integer.MAX_VALUE,则返回Integer.MAX_VALUE。
boolean isEmpty():检查列表是否为空,为空则返回true。
boolean contains(Object o): 如果此列表包含指定的元素,则返回true。
E get(int index): 获取指定位置的元素。
ListIterator listIterator(): 返回迭代器对象,用来遍历集合中元素。
Iterator iterator(): 返回迭代器对象,用来遍历集合中元素。
<T> T[ ] toArray(T[[ ] a): 把列表转成指定类型的数组,数组长度等于列表长度。
②遍历List有几种方式?
- for循环遍历:基于计数器,在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
- 增加for遍历,遍历的时候获取不了下标。
- 迭代器遍历:迭代器是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。
- foreach遍历:写法简洁。
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
list.add("111");
list.add("222");
list.add("333");
list.add(444);
System.out.println("通过下标遍历:");
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
System.out.println(o);
}
System.out.println("增强for遍历:");
for (Object o : list) {
System.out.println(o);
}
System.out.println("拉姆达表达式遍历:");
list.forEach(element-> System.out.println(element));
System.out.println("迭代器遍历:");
Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
③如何边遍历边移除Collection集合中的数据
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
list.add("111");
list.add("222");
list.add("333");
list.add(444);
System.out.println("通过下标删除:");
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
/**
* 这一步在删除的时候,有时候删的不是你想要删的数据。List的底层是一个动态数组,每删一个,所有元素都要向前移动一位
*/
list.remove(i); //通过下标进行删除(无效删除)
System.out.println(o);
}
for (Object o : list) {
//★此处容易出错
list.remove(o); //通过元素去删除(运行报错)
}
System.out.println("迭代器遍历:");
Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
iterator.remove(); //通过迭代器删除(唯一有效删除)
}
System.out.println(list);
}
④Iterator 和 ListIterator 有什么区别?
区别 | Iterator | ListIterator |
---|---|---|
遍历对象不同 | 可以遍历Set和List集合 | 只能遍历List |
遍历方式不同 | 只能单向遍历 | 可以双向遍历(前/后) |
额外功能 | ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 |
⑤ArrayList的扩容规则 ★★
刚开始创建一个ArrayList对象时,开始起始容量为0的空数组,
只有调用add()方法时,才会触发扩容机制,起初容量为10 ,当用add()方法添加元素时,会先用这个数组的最大长度和(数组长度+1)比较,若最大长度小于(数组长度+1)就会进行如下扩容:
int newCapacity = oldCapacity + (oldCapacity >> 1); //也可以简单理解扩容为原先的1.5倍
扩容完成之后,会把原先数组中的内容进行Arrays.copyof(old,new)进行复制到新的数组中去。重复上述 二三步骤。
⑥说一下 ArrayList 的优缺点
联系顺序表的工作特点。
查找快,在表的尾部添加元素较快。
删除慢,插入元素慢,还需要移动数据。
优点
- ArrayList底层是以数组实现,查找时,随机访问速度很快。
- 顺序添加元素时速度很快。
缺点
- 删除元素的时候,需要做一次元素复制操作。如果复制的元素很多,就会比较耗费性能。
- 插入元素时,也需要复制元素操作。
⑦如何实现数组和 List 之间的转换?★
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
list.add("111");
//list集合转换为数组
Object[] array = list.toArray();
//数组转换为list
List<Object> asList = Arrays.asList(array);
}
⑧List如何转换为Map ★
通过遍历List,再将List中的数据传递给Map集合中
List<String> list = Arrays.asList("apple", "banana", "cherry");
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < list.size(); i++) {
map.put(i, list.get(i));
}
System.out.println(map);
⑨ArrayList 、LinkedList的区别是什么? ★
区别 | ArrayList | LinkedList |
---|---|---|
数据结构 | 动态数组 | 双向链表 |
随机访问效率 | 高 | 低 |
增加和删除效率 | 尾部元素增加,删除效率高(可能会影响其他数据的下标) | 非尾部元素,效率更高 |
内存空间占用 | 相对少 | 更占内存(双向链表,还要存储两个引用) |
线程安全 | 不安全,异步,效率低 | 不安全,异步,效率低 |
使用上 | 频繁读取集合中的元素时,推荐 | 插入和删除操作较多时,推荐s⑩ |
⑩ArrayList 、Vector的区别是什么?★
ArrayList | Vector | |
---|---|---|
线程安全 | 异步,线程不安全 | 同步,线程非安全 |
扩容方式扩容方式 | 扩容一半 | 扩容一倍 |
性能 | 性能较优,推荐使用 | 不及ArrayLshiyi |
⑪插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述ArrayList、Vector、LinkedList 的存储性能和特性?
联想底层进行回答。
ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。
LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。
⑫多线程情况下,如何保证ArrayList是线程安全的?
ArrayList不是线程安全的,如果遇到多线程场景,可以通过Collections的synchronizedList方法将其转换成线程安全的容器后再使用:
List<String> synchronizedList = Collections.synchronizedList(list); synchronizedList.add("aaa"); synchronizedList.add("bbb"); for (int i = 0; i < synchronizedList.size(); i++) { System.out.println(synchronizedList.get(i)); }
这种方式相当于对ArrayList加锁
2.Set接口
①常用set有哪些?各自有什么特点 ★★
实现类 | HashSet | LinedHashSet | TreeSet |
---|---|---|---|
底层 | 数组+单向链表+红黑树(底层HashMap) | 数组+双向链表+红黑树(底层LinkedHashMap) | 红黑树(底层TreeMap) |
存储特点 | 无序,不可重复 | 同左 | 默认自然排序 |
扩容方式 | 默认初始容量16,加载因子0.75 | 同左 |
② set使用时需要注意哪些?怎么判断两个对象相等?
HashSet去重时需要重写equals(Object o)和hashCode()方法。
一、重写equals(Object o)的步骤:
1.比较2个对象是否是同一个对象 if (this == o) return true;
2.判断被比较的对象是否为空 if (o == null) return false; 比较2个对象的类对象对否是同一个对象 if (this.getClass() != o.getClass()) return false;
3.把被比较的对象强制转换成当前对象的类型
4.逐个比较2个对象的属性是否相等。
二、重写hashCode()方法的原则: 1.和equals方法保持一致性,指的是equals方法返回true的对象,hashCode需要保持一致。
2.尽可能保证不同对象的hashCode不一致,如果很多对象返回的hash值都一样,会大大降低hash的效率。
public class person { String id; String name; String sex; @Override public boolean equals(Object o) { if (this == o) return true; //判断地址是否相等 if (o == null || getClass() != o.getClass()) return false; //判断是否为空 ;判断两个对象是否为同一类型对象 person person = (person) o; //再比较对象中的类型是否相等(分析一:) return Objects.equals(id, person.id) && Objects.equals(name, person.name) && Objects.equals(sex, person.sex); } @Override public int hashCode() { //生成一个hash值(分析二:) return Objects.hash(id, name, sex); } } //分析一:是如何比较的 public static boolean equals(Object a, Object b) { // 比较地址并且也比较到值了 return (a == b) || (a != null && a.equals(b)); } //分析二:如何生成hash值的(底层根据属性去随机生成键值) public static int hash(Object... values) { return Arrays.hashCode(values); } public static int hashCode(Object a[]) { if (a == null) return 0; int result = 1; for (Object element : a) result = 31 * result + (element == null ? 0 : element.hashCode()); return result; }
③ 重写equals()为什么还要重写hashcode()? ★
结论:
情形一:没有重写equals,比较的是地址值。
情形二:重写了equals,可以比较对象的值。
情形三:既重写了equals,又重写了hashCode是为了满足HashSet、HashMap等此类集合的相同对象的不重复存储。
解释:
如果只重写equals方法,不重写hashCode。就可能导致a.equals(b)这个表达式成立,但是hashCode却不同。那么只重写了equals方法的这个对象,在使用散列集合进行存储的时,就会出现问题,因为散列集合是使用hashCode来计算key的位置。加入要存储两个完全相同的对象,但是有不同的hashCode,存储在hash表的不同位置,当我们想要去根据这个对象去获取对象时,就会出现一个悖论:一个完全相同的对象,会存储在hash表的两个位置。这样做之后,会使得我们在程序中会出现一些不可预料的错误。
理论:
可以结合hashMap的put操作原理理解。核心一点:先比较hash值,再比较equals方法的返回值。
④简述Set的两种实现的排序接口
实现Comparable接口是默认自然排序
实现Comparator接口可以自定义排序
//使用comparable接口
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentComparator implements Comparator {
Integer id;
String name;
@Override
public int compare(Object o1, Object o2) {
StudentComparator studentComparator1=(StudentComparator)o1;
StudentComparator studentComparator2=(StudentComparator)o2;
return studentComparator1.id.compareTo(studentComparator2.id);
}
}
class TestStudentComparator{
public static void main(String[] args) {
ArrayList<StudentComparator> studentComparators = new ArrayList<>();
ArrayList<StudentComparator> students = new ArrayList<>();
students.add(new StudentComparator(1,"zs1"));
students.add(new StudentComparator(3,"zs3"));
students.add(new StudentComparator(3,"zs3")); //list可以添加重复数据
students.add(new StudentComparator(3,"zs4"));
students.add(new StudentComparator(4,"zs3"));
students.add(new StudentComparator(2,"zs2"));
System.out.println("***************内部comparator****************");
//使用自定义内存比较器的排序(id升序)
Collections.sort(students,new StudentComparator());
for (StudentComparator student : students) {
System.out.println(student);
}
System.out.println("***************外部comparator****************");
//学生对象进行排序(自定义name升序,此时内部比较器失效)
Collections.sort(students, new Comparator<StudentComparator>() {
@Override
public int compare(StudentComparator studentComparator, StudentComparator t1) {
return studentComparator.name.compareTo(t1.name); //根据name排序
}
});
for (StudentComparator student : students) {
System.out.println(student);
}
System.out.println("***************外部comparator(Lambda)****************");
//id降序
Collections.sort(students,(s1,s2)->{return s2.id.compareTo(s1.id);});
for (StudentComparator student : students) {
System.out.println(student);
}
}
}
//comparator接口
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentComparator implements Comparator {
Integer id;
String name;
@Override
public int compare(Object o1, Object o2) {
StudentComparator studentComparator1=(StudentComparator)o1;
StudentComparator studentComparator2=(StudentComparator)o2;
return studentComparator1.id.compareTo(studentComparator2.id);
}
}
class TestStudentComparator{
public static void main(String[] args) {
ArrayList<StudentComparator> studentComparators = new ArrayList<>();
ArrayList<StudentComparator> students = new ArrayList<>();
students.add(new StudentComparator(1,"zs1"));
students.add(new StudentComparator(3,"zs3"));
students.add(new StudentComparator(3,"zs3")); //list可以添加重复数据
students.add(new StudentComparator(3,"zs4"));
students.add(new StudentComparator(4,"zs3"));
students.add(new StudentComparator(2,"zs2"));
System.out.println("***************内部comparator****************");
//使用自定义内存比较器的排序(id升序)
Collections.sort(students,new StudentComparator());
for (StudentComparator student : students) {
System.out.println(student);
}
System.out.println("***************外部comparator****************");
//学生对象进行排序(自定义name升序,此时内部比较器失效)
Collections.sort(students, new Comparator<StudentComparator>() {
@Override
public int compare(StudentComparator studentComparator, StudentComparator t1) {
return studentComparator.name.compareTo(t1.name); //根据name排序
}
});
for (StudentComparator student : students) {
System.out.println(student);
}
System.out.println("***************外部comparator(Lambda)****************");
//id降序
Collections.sort(students,(s1,s2)->{return s2.id.compareTo(s1.id);});
for (StudentComparator student : students) {
System.out.println(student);
}
}
}
⑤Comparable和Comparator区别 ★
当两者同时存在时,就近原则使用
区别 | Comparable | Comparator |
---|---|---|
排序逻辑 | 排序逻辑必须在待排序对象的类中,故称为自然排序 | 排序排序逻辑可以在内部,也可以在外部(就近原则) |
实现 | 实现Comparable接口 | 实现Comparator接口 |
排序方法 | int compareTo(Object o1) | int compara(o1,o2) |
触发排序 | Collections.sort(List) | Collections.sort(List,Comparator) |
接口所在包 | java.lang.Comparable | java.util.Comparator |
优点 | 重用度高 | 灵活度高(可以多次定义不同规则),耦合度低(不用改变比较对象类的本身) |
缺点 | 灵活度低,耦合度高 | 重用度低 |
⑥HashSet的实现原理
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet 不允许重复的值。
⑦HashSet如何检查重复?HashSet是如何保证数据不可重复?
向HashSet中add()元素时,判断元素是否存在的依据,不仅仅要比较hash值,同时还要结合equals()方法比较。
HashSet中add()方法会使用HashMap的put()方法
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap 比较key是否相等是先比较hashcode 再比较equals )。
//HashMap的部分源码 private static final Object PRESENT = new Object(); private transient HashMap<E,Object> map; public HashSet() { map = new HashMap<>(); } public boolean add(E e) { // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值 return map.put(e, PRESENT)==null; }
⑧HashSet与HashMap的区别? ★
HashMap | HashSet | |
---|---|---|
实现接口不同 | 实现Map接口 | 实现Set接口 |
存储类型不同 | 存储键值对(双列) | 仅存储对象(单列) |
添加方法不同 | put() | add() |
计算HashCode不同 | HashMap使用键(key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么就返回false。 |
效率上 | HashMap是根据唯一的键来生成hashcode去找取对象的位置。 | 根据对象来计算hashcode值,对象可以有多个,生成的比较慢。 |
3.Map接口
①Map的遍历方式
大体上三种:
keyset获取所有键, entries键值对,lambda遍历
public static void main(String[] args) {
HashMap<Object, Object> map = new HashMap<>();
map.put("1",1);
map.put("2",2);
//遍历方式一:(通过entry直接获取键值)
Set<Map.Entry<Object, Object>> entries = map.entrySet();
System.out.println("**********entry的增强for***********");
for (Map.Entry<Object, Object> entry : entries) {
Object key = entry.getKey();
Object value = entry.getValue();
System.out.println(key +":"+value);
}
Iterator<Map.Entry<Object, Object>> iterator = entries.iterator();
System.out.println("**********entry的迭代器***********");
while (iterator.hasNext()) {
Map.Entry<Object, Object> next = iterator.next();
Object key = next.getKey();
Object value = next.getValue();
System.out.println(key+":"+value);
}
//方式二:找到对应的keySet集合,通过key来获取对应的值
Set<Object> keySet = map.keySet();
System.out.println("**********keySet的增强for***********");
for (Object o : keySet) {
Object o1 = map.get(o);
System.out.println(o+":"+o1);
}
System.out.println("**********keySet的迭代器***********");
Iterator<Object> keyIterator = keySet.iterator();
while (keyIterator.hasNext()) {
Object next = keyIterator.next();
Object o = map.get(next);
System.out.println(next+":"+o);
}
//方式三:通过lambda去遍历
System.out.println("**********lambda***********");
map.forEach((key,value)-> System.out.println(key+":"+value));
}
② 常用Map有哪些,各自特点 ★
- HashMap(散列表,存储键值对、不是线程安全的、允许存放null)JDK1.8之前HashMap由数组+链表组成。数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(拉链法 去解决冲突)。JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转换为红黑树,以减少搜索时间。
- LinkedHashMap(将HashMap和双向链表合二为一,保持插入和访问顺序)继承自HashMap,所以它的底层仍是基于拉链式散列结构即由数组和红黑树组成。此外,LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- TreeMap(二叉树结构,对键自然排序)
- HashTable(散列表结构,线程安全、键和值都不为空)
- ConcurrentHashMap(JDK1.5之后替换HashTable)
③什么是Hash算法
哈希算法是指把任意长度的二进制映射为固定长度较小的二进制值,这个较小的二进制值叫哈希值。
④HashMap的put方法的具体流程?HashMap在JDK1.7和1.8的区别 ★★
插入具体流程:
在new HashMap()时,底层会初始化数组Entry[] table=new Entry[16],当你要进行put(key,value)时,会先拿key经过hashcode()生成哈希值1,然后哈希值1又经过hash()方法生成哈希值2,最后又拿哈希值2经过算法 indexof()生成在数组中的位置索引i。
在添加的时候先查看该索引位置上是否有值,没值可以直接插入。有值再比较生成的哈希值
2,不同直接插入,相同比较equals(),不同直接插入,相同替换。
区别:
JDK7 JDK8 数据结构 数组+链表 数组+链表+红黑树 new HashMap时 初始化了一个长度为16的数组 刚开始只是创建了实例,并没有初始化数组,只有当向这里面插入数据时,才会对数组进行初始化 链表插入 头插法 尾插法 底层定义 Entry内部类 Node内部类 存放数据规则 无冲突时,存放数组;有冲突时,存放链表 无冲突时,存放数组;有冲突时且链表长度<8:存放单链表,当链表长度 > 时:树化成为红黑树。 扩容规则 (数组扩容)当数组长度大于门限值并且每个数组位置都有值时,会扩容为原先的2倍 (数组扩容同jdk7,链表扩容)当对应数组位置上的结点超过8,会树化红黑树;当红黑树结点个数小于6,会退化单链表 JDK7的门限值:数组长度*默认加载因子(16 * 0.75),默认临界值12
二次Hash作用:减少碰撞次数。
⑤HashMap类的常用方法
//增
put(key,value);
//删
remove(key);
clear(); //清空
//改
put(key,value);
//查
get(key);
containsKey(key);
containsValue(value);
size(); // 键值对数量
isEmpty(); //是否为空
values(); //得到所有的value
keySet(); //返回所有的keySet集合
entrySet(); //获取所有键值对
⑥HashTable与HashMap区别 ★
不同点 | HashMap | HashTable |
---|---|---|
底层 | 数组+链表+红黑树(JDK8,链表大于阈值8,会树化) | 数组+链表(HashTable不会树化) |
线程安全 | 非线程安全 | 线程安全 |
效率 | 效率高 | 效率低 |
扩容大小 | 初始值为16,默认扩容两倍 | 初始值为11,默认扩容 2n+1 |
存储特点 | 存储元素支持空值空键 | 不支持空值空键 |
使用情形 | 推荐使用 | 不推荐使用 |
hash次数 | 在计算下标时,会进行两次hash算法 | 只进行了一次hash算法 |
⑦ConcurrentHashMap是如何保证的线程安全的?想要线程安全的HashMap怎么办?
解决HashTable效率低下的问题,concurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程线程安全,尽可能减少了性能损耗。
JDK1.7:使用分段锁,将一个Map分为了16个段,每个段都是一个小的hashmap,每次操作只对其中一个段加锁JDK1.8:采用CAS+Synchronized保证线程安全,每次插入数据时判断在当前数组下标是否是第一次插入,是就通过CAS方式插入,然后判断f.hash是否=-1,是的话就说明其他线程正在进行扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保证并发下移除元素安全
(1)使用ConcurrentHashMap
(2)使用HashTable
(3)Collections.synchronizedHashMap()方法
五、线程
1.多线程
①线程创建的方式 ★
//线程创建的方式
public class ThreadTest {
public static void main(String[] args) throws Exception {
//方式一:通过继承Thread类,重写run方法
A a = new A();
a.run();
//方式二:实现Runnable接口
B b = new B();
b.run();
//方式三:实现Callable接口
C c = new C();
c.call();
}
}
//方式一:继承Thread类,重写run方法
class A extends Thread {
@Override
public void run() {
System.out.println("you jump,i jump");
}
}
//方法二:实现Runnable接口,重写run方法
class B implements Runnable{
@Override
public void run() {
System.out.println("i jump,you jump");
}
}
class C implements Callable {
@Override
public Object call() throws Exception {
System.out.println("Callable jump");
return null;
}
}
①继承Thread类
1.创建一个继承Thread的类。
2.重写Thread类中的方法。
3.创建实现类对象。
4.通过此对象调用start()。
②实现Runnable接口
1.创建一个类,实现Runnable接口。
2.实现Runnable接口中的抽象方法。
3.创建一个实现类对象。
4.将对象作为参数传递到Thread类的构造器中,创建Thread类对象。
5.通过Thread类对象去调用start()。
③实现Callable接口
1.创建一个类,实现Callable接口。
2.实现接口中的call()方法。
3.创建实现类对象。
4.将实现类对象作为参数传递到FutureTask构造器中,创建FutureTask对象。
5.将创建好的FutureTask对象作为参数传递到Thread构造器中,创建Thread对象。
6.通过Thread对象去调用start()方法。
② 实现方式的区别
1.继承Thread
优点:子类继承Thread类,子类本身就是线程类,可以直接使用相关方法。
缺点:不能再继承其他类,扩展性不好。
2.实现Runnable接口
优点:避免了 java单继承的局限性,提高了程序的可扩展性,降低了程序的耦合度
3.实现Callable接口 优点:Callable接口更灵活、异常可以抛出、可以返回线程执行结果
继承Thread类 vs 实现Runnable接口
开发中:优先选择实现Runnable接口中的方式。
原因: 1.实现的方式没有类的单继承的局限性。
2.实现的方式更适合来处理多个线程有共享数据的情况。
两者的联系:public class Thread implements Runnable 。
相同点:两种方式都需要重写run() 将线程要执行的逻辑声明在run()中。
实现Runnable接口 vs 实现Callable接口
1.如何理解实现Runnable接口的方式创建多线程比实现Runnable接口创建线程强大?
a. call()可以有返回值
b. call()可以抛出异常,被外面的操作捕获,获取异常信息
c. Callable支持泛型
③ 什么是并发和并行★
并发:多个事件在同一个时间间隔内发生
并行:两个或两个以上事件或活动在同一时刻发生,在多道程序环境下,并行使多个程序同一时刻可在不同CPU上同时执行。
④线程的生命周期
新建:
使用new()方法,new出来的线程,此时仅仅由Java虚拟机为其分配内存,并初始化成员变量的值。此时仅仅是个对象。
就绪:
调用的线程的start()方法后,这时候线程处于等待CPU分配资源的阶段。当线程进入就绪态后,JAVA虚拟机会为其创建方法调用栈和程序计数器。
运行:当就绪态的线程被调度并获得CPU资源时,便进入运行状态(会执行run方法)
阻塞:在运行状态时,可能因为某些原因导致运行状态的线程变成了阻塞态:
①等待I/O的输入输出②等待网络资源(网速问题)
③调用sleep()方法,需要等待sleep时间结束
④调用wait()方法,需要调用notify()唤醒
⑤其他线程执行join()方法,当前线程会阻塞,需要等待其他线程执行完
死亡:①run()/call()方法执行完,线程正常结束
②线程抛出一个未捕获的Exception或Error
③ 直接调用线程的stop()方法结束该线程
⑤sleep和wait的区别
相同点:一旦执行,都可以使得当前线程进入阻塞态
区别 | Sleep() | Wait() |
---|---|---|
声明位置不同 | Thread类中声明sleep() | Object类中wait() |
调用要求不同 | sleep()可以在任何需要的场景下调用 | wait()必须使用在同步代码块中 |
是否释放同步监视器(锁)不同 | 如果两个方法都使用在同步代码块或同步方法中,调用sleep()会释放锁 | 调用wait()方法会释放锁 |
语法不同 | 进程在调用sleep()方法后,一段时间会自动恢复 | 进程在调用wait()方法后,必须通过notify()或notifyAll()方法去唤醒进程,否则一直处于阻塞状态。 |
⑥ 同步代码块、锁、死锁 ★
锁:通过synchronized关键字锁定共享对象
方法名前synchronized关键字标记
同步代码块用synchronized(Object …){…}标记锁定的代码
当线程执行由synchronize关键字控制的代码时,就锁定了共享对象。其他线程如果需要操作该对象。就必须等待该线程执行完受保护的代码
死锁:synchronized关键字可以解决多线程同步问题,但是如果处理不当,可能会产生新的问题----死锁。(当多个线程分别占用某个资源,并等待其他线程释放该资源时,可能产生死锁)
⑦ 什么是乐观锁和悲观锁 ★
-
乐观锁(version)
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是在对数据进行更新时,会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
- 使用场景:乐观锁适用于写比较少的情况下(多读场景),即冲突发生很少的时候,这样可以省去了锁的开销,加大了整个系统的吞吐量
- 版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值和当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
- CAS(compare and swap)比较与交换
是一种有名的无锁算法。无锁编程:在不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。CAS算法设计到以下三个操作:- 需要读写的内存值V
- 进行比较的值A
- 要写入的新值B当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作。(比较和替换是一个原子操作)
-
悲观锁(synchronized)
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
⑧ 死锁的必要条件 ★
保持并请求:请求其他资源的同时对自己手中的资源保持不放
闭环:在相互等待资源的过程中,形成一个闭环
互斥使用资源:同一资源同时只能有一个线程读取
不可被剥夺:不能强行剥夺线程占用的资源
⑨ Synchronized和Lock的区别 ★
synchronized是一个关键字,Lock是一个类
synchronized在发生异常时会自动释放锁,Lock需要手动释放锁
synchronized是可重入锁、非公平锁、不可中断锁,Lock是可重入锁,可中断锁,可以是可公平锁
⑩ yield()和jion()的区别
区别 | yield | join |
---|---|---|
类型 | yield()方法是Thread类中一个静态方法,调用该方法会让当前线程让出CPU,让其他线程有机会执行,但是yield()不会释放锁。 | jion()方法是Thread类中一个实例方法,调用该方法会让当前线程等待另一个线程执行完后再继续执行。 |
是否影响线程执行顺序 | 不会影响其他线程执行顺序,只是当前线程让出CPU。 | 会影响线程的执行顺序,调用join()方法的线程会等待被调用的线程执行完毕后再执行。 |
使用场景 | 使用场景较少,通常只在调试和测试时使用 | 使用场景较为广泛,通常在多线程编程 |
⑪简述下你对锁的认识及实现方式 ★
为什么要上锁?
主要还是多线程的原因。多线程可能对同一块数据同时操作,数据可能会出现不一致的情况。eg:A线程去对数据做读操作,同一时刻下 B线程去对数据做写操作。
---> 引出锁
锁在我的理解里面就是系统调度的策略,控制数据操作的一种工具。
⑫volatile ★
volatile是JVM提供的轻量级的同步机制。
volatile关键字可以保证并发编程三大特性(原子性、可见性、有序性)中的可见性和有序性,不能保证原子性。
可见性:加了volatile关键字修饰的变量,只要有一个线程将内存中的变量做了修改,其他线程将马上收到通知,立即获得最新值。
当写线程写一个volatile变量,JVM会把该线程对应的本地工作内存中的共享变量刷新到主存中。当线程读一个volatile变量时,JVM会把线程对应的本地工作内存置为无效,线程将到主存中重新获取共享变量。
有序性:禁止指令重排序。
计算机在执行程序时,为了提高计算性能,编译器和处理器常常会对指令进行重排序
⑬JUC常用辅助类或Java的同步包下面有哪些子类 ★
2.线程池
① 什么是线程池,线程池有什么特征?★
线程池是一种用于管理和复用线程的机制,它可以在应用程序中预先创建一定数量的线程,然后根据需要将任务提交给这些线程来执行,从而避免了频繁创建和销毁线程的开销
特征:
- 提高应用程序的性能,减少线程创建和销毁的开销,避免线程过多导致系统资源不足的问题。
- 可以管理线程的数量,避免线程过多或过少,从而保证系统的稳定性和可靠性。
- 重复使用线程,避免了频繁创建和销毁线程的开销,提高了应用程序的效率和响应速度。
- 提供可调节的线程数量,可以将应用程序的负载情况动态调整线程数量,从而保证系统的性能和效率。
- 可提供任务队列,可以将任务提交到队列中,等待线程中的线程来执行,从而实现异步处理和调度。
② Java中有几种线程池,都分别是哪些?各自有什么特点? ★
1.四种线程池:
① CachedThreadPool(可回收缓存线程池)
特点:创建一个缓存线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,若无可回收,则新建线程,空闲超过60秒销毁线程。但是如果创建非常多的线程,甚至会OOM(Out of Memory)。
应用场景:适合、密集、短时间的任务。
② FixedThreadPool(定长线程池)
特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等到。但是问题是堆积的请求处理队列可能会耗费非常大的内存,甚至会OOM(内存溢出)。
应用场景:控制线程的最大并发数。
③ schduledPool(定时线程池)
特点:可以给线程定时
应用场景:给线程定时,执行周期性任务时。
④SignalPool(单线程化线程池)
特点:
a.建造一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照制定顺序执行。
b.只有一个核心线程,无非核心线程,执行完立即回收。即使出现异常也能保障一个线程被创建在线程池中。
应用场景:不适合并发并发
⑤newWorkStealingPool(任务窃取线程池)特点:
把一个任务拆分成多个“小任务”,把这些“小任务”分发到多个线程上执行。这些“小任务”都执行完后,再将结果合并。
当线程发现自己的队列中没有任务了,就会到别的线程的队列里获取任务执行。可以简单理解为“窃取”。
2.线程池的作用
①降低资源消耗 :通过重复利用已创建的线程来降低线程的创建和销毁造成的消耗。
②加快响应速度:当任务到达的时候,任务可以不需要等到线程创建就能执行任务。
③提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一进行分配,调优和监控。
③ 叙述线程池的创建和使用方式? ★
线程池的创建方法总共有7种,但总体来说可分为2类
①通过 ThreadPoolExecutor 创建的线程池 ②通过Executors 创建的线程池
线程池的创建方式总共包含以下7种(6种是通过Excecutor创建的,1种是通过ThreadPoolExcecutor)
1. Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待; 2. Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程; 3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序; 4. Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池; 5. Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池; 6. Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。 7. ThreadPoolExecutor:最原始的
具体线程创建的方式:
public static void main(String[] args) throws ExecutionException, InterruptedException { // 1.单一线程池(newSingleThreadExecutor) /** * 单线程的线程池有什么意义? * ①复用线程 * ②单线程的线程提供了任务队列和拒绝策略(有点类似于排队处理,当队列满了之后,会拒绝接下来进入队列的线程) */ ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0; i < 5; i++) { executorService.submit(()-> System.out.println("单一型线程名:"+Thread.currentThread().getName())); } //2.固定数量的线程池(newFixedThreadPool) ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); //方式一: Future<String> submit = fixedThreadPool.submit(() -> Thread.currentThread().getName()); System.out.println("方式一 -> 固定数量的线程型:"+submit.get()); //方式二: fixedThreadPool.execute(()-> System.out.println("方式二->固定数量的线程型:"+Thread.currentThread().getName())); //3.带缓存的线程池(newCachedThreadPool) /** * 带缓存线程池特点: * 优点:线程池会根据任务数量创建线程池,并且在一定时间内可以重复使用这些线程,产生相应的线程池。 * 缺点:适应于短时间有大量任务的场景,会占用很多的资源 */ ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 3; i++) { int finalI = i; cachedThreadPool.submit(() -> System.out.println("i : " + finalI + "|缓存线程名称:" + Thread.currentThread().getName())); } //4.执行定时任务的线程(newScheduledThreadPool) //延迟执行(一次) ScheduledExecutorService scheduledThreadPool1 = Executors.newScheduledThreadPool(3); System.out.println("延迟一次:添加任务的时间:"+ LocalDateTime.now()); scheduledThreadPool1.schedule(()-> System.out.println("执行子任务:"+LocalDateTime.now()),3,TimeUnit.SECONDS); //固定频率执行 ScheduledExecutorService scheduledThreadPool2 = Executors.newScheduledThreadPool(3); System.out.println("固定频率:添加任务的时间:"+ LocalDateTime.now()); scheduledThreadPool2.scheduleAtFixedRate(()-> System.out.println(Thread.currentThread().getName()+"执行任务:"+LocalDateTime.now()),2,4, TimeUnit.SECONDS); //5.定时任务单线程 ExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor(); //6.根据当前CPU生成线程池(newWorkStealingPool) ExecutorService workStealingPool = Executors.newWorkStealingPool(); //7.通过ThreadPoolExecutor方式去创建最原始的线程池 }
④ execute和submit的区别 ★
execute | submit | |
---|---|---|
提交任务类型 | 只能提交Runnable类型的任务 | 既能提交Runnable类型也能提交Callable类型任务 |
有无返回值 | 无返回值 | 有返回值 |
⑤简单描述下线程池的属性及含义
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize:池中所保存的线程数,包括空闲线程maximumPoolSize: 池中允许的最大线程数
keepAliveTime:当线程数大于核心时,会终止此前多余的空闲线程等待新任务的最长时间
unit:keepAliveTime参数的时间单位
workQueue: 执行前用于保持任务的队列。此队列仅保持由execute()方法提交的Runnable任务。
threadFactory:执行程序创建新线程时使用的工厂。
handler:由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
ThreadPoolExecutor是Executors类的底层实现
六、IO流
①IO流的分类
输入流/输出流、字节流、子符流、缓冲流、转换流、对象流、打印流
1.字节流字节输入流:InputStream
常用子类:FileInputStream
字节输出流:OutputStream
常用子类:FileOutputStream
2.字符流字符输入流:Reader
常用子类:FileReader
字符输出流:Writer
常用子类:FileWriter
3.缓冲流:减少IO次数,提高IO次数字节缓冲输入流:BufferedInputStream
字节缓冲输出流:BufferedOutputStream
字符缓冲输入流:BufferedReader
字符缓冲输出流:BufferedWriter
4.对象流
对象输入流:ObjectInputStream对象输出流:ObjectOutputStream
5.打印流打印流:PrintStream
②IO流实现文件上传
1.先获取文件
2.创建输入输出流
3.循环读取文件,然后进行上传
4.关闭输入输出流
③序列化和反序列化
序列化
概念:Java序列化就是把Java对象转换为字节序列的过程
作用:在传递和保存对象时,保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
优点:①将对象转换为字节路存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等到下一次把JVM启动时,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制能够减少存储空间(永久性保存对象)。
②序列化成字节流式的对象可以进行网络传输,方便了网络传输。
③通过序列化可以在进程间传递对象。
实现步骤:
先让实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。然后在任选一个输出流。
反序列化概念:Java反序列化就是把字节序列恢复到Java对象的一个过程。
作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
七、JDK新特性
①JDK版本新特性 ★
JDK5
- 泛型
- 增强for循环
- 元注解
- 可变参数
JDK7
- switch语句块中可以字符串中作为分支条件
- 在一个语句块中捕捉多种异常
- 数值类型可以是二进制
- 支持try-With-resource
JDK8
- Lambda表达式
- 函数式接口
- Java8增强接口
- JDK9
1.接口方法可以使用private来修饰
2.设置G1为JVM默认垃圾收集器
3.支持http2.0和websocket的API- JDK10
1.并行执行Full GC,来优化G1的延迟- JDK11
1.对Stream、Optional、集合API进行增强- JDK12
1.Swith 表示语法扩展,可以有返回值
2.G1收集器的优化
② Lambda表达式作用?使用场景? ★
任何一个接口里面只有一个普通方法的时候都可以使用
列表迭代遍历:forEach(x->sout(x))
Comparator中自定义排序:
String[] players = {"d","c","b","a"}; Comparator<String> sortByName = (String s1, String s2) -> (s1.compareTo(s2)); 或者 Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2)));
③函数式接口
特点:
只能有一个抽象方法
不能是普通方法,可以是重写Object中的方法
可以有多个常量,默认方法,静态方法
八、设计模式 3+7+23
① 谈一谈你对设计模式的理解
设计模式:是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
② 设计模式包含哪3种设计类型?
- 创建型模式:提供创建对象的机制,能够提升已有代码的灵活性和可复用性。
- 结构型模式:介绍如何将对象和类组成比较大的结构,并同时保持结构的灵活和高效。
- 行为模式:负责对象之间高效交互。
③ 7个基本原则有哪些?
- 单一职责原则:一个类只负责一个领域中的相应职责
- 开闭原则:一个软件实体应当对扩展开放,对修改关闭
- 依赖倒转原则:抽象不应该依赖于细节,细节应当依赖于抽象。(要针对接口抽象编程,而不是针对实现编程)
- 接口隔离原则:使用多个专门的接口,而不使用单一的总接口(客户端不应该依赖那些它不需要的接口)
- 迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用。
- 里式代换原则:所有引用父类的地方必须能透明地使用其子类的对象。(保持父类方法不被覆盖)
- 合成复用原则
③ 23种设计模式
创建型模式
- 单例模式:对象永远只会生成同一个对象,不论如何创建都返回同一个对象,是最简单的设计模式
- (简单)工厂模式:简单工厂模式就好比我们在家吃饭,只需要父母,静静等待就行,不需要知道具体的制作过程。
- 抽象工厂模式:可以理解成一个大食堂,这个食堂有各种各样的食品,我们需要要下单即可送到我们身边。(与简单工厂模式的区别是生产产品的种类有不同,抽象工厂模式只能生产不同种类的产品,简单工厂只能生产单一产品)
- 原型模式:原型模式即克隆模式,即创建重复的对象,但又可以保证性能。
- 建造者模式:对于复杂对象,我们是不可能一下将这个对象创建出来,我们需要将这个对象进行拆解,然后按照步骤来创建这个复杂的对象,实现了创建复杂对象的灵活性和可读性。
结构型模式
行为模式
九、JVM
①JVM组成
主要分为三个部分:
①类装载器:用来装载 .class文件
②执行引擎:执行字节码、或者本地方法
③运行时数据区:方法区、堆、java栈、PC计数器、本地方法栈
② 类加载过程
①加载
获取一个类的二进制字节流,然后将字节流中的静态板块块加载到方法区中,最后再堆中创建一个class对象,作为对静态内容访问的入口。
②验证
校验 二进制字节流是否符合要求。
③准备
将静态变量初始化,设置默认值。
④解析
虚拟机常量池中的符号引用,转换成为直接引用。
⑤初始化
对静态变量,静态代码块进行初始化。
⑥使用(这个忘记了) 初始化完了要使用
⑦卸载 使用完了要卸载。
③ 类什么时候会被加载?
①new 对象时
②调用类的静态方法时
③ 初始化一个类的子类(先父后子)
④访问某个类或接口的静态变量,或者对该静态变量赋值
⑤反射,class.forName(“类的全路径”);
⑥ JVM启动时标明的启动类
④ 谈一谈你对双亲委派的理解
如果一个类加载器需要加载类,那么它会把这个类加载请求给父类加载器去完成,如果父类还有父类则接着委托,一直递归到顶层,当父加载器无法完成这个请求时,子类才会加载。
好处:
1.类加载器具备了一种带有优先级的层次关系,这种层次关系保证了Java程序的稳定运作。
eg:例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此 类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同 一个类。 2.提高安全性,防止危险代码植入。
eg:如果有人想替换系统级别的类:String.java。篡改它的实现,这种机制下这些系统的类已经被BootStrap classLoader加载过了(为什么? 一个类需要加载时,最先尝试加载的就是BootStrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止危险代码的植入。
⑤ 内存空间有哪些?哪些是线程共享的? ★
- 程序计数器(PC):存放下一条指令的地址
- Java虚拟机栈:存储方法的局部变量、对象的应用、方法口的信息。每一个线程都有一个私有的Java虚拟机栈,每个方法在执行时都会创建一个栈帧用于存储这些信息,当方法执行完成时,栈帧会被出栈。
- 本地方法栈: 用于存储本地方法的…。与Java虚拟机栈类似。
- Java堆:存放对象实例、数组。是Java虚拟机管理的内存中最大的一块,当Java堆的空间不足时,会触发垃圾回收机制进行自动回收。
- 方法区:存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 运行时常量池:存储编译器生成的各种字面量和符号引用,包括类和接口的全限定名、字段和方法的名称和描述符等信息。
⑥ 常见的GC垃圾回收算法? ★
算法思想 | 优点 | 缺点 | |
---|---|---|---|
标记清除法 | 为每个对象存储一个标记位,记录对象的状态(活着还是死亡)。在算法执行过程中分为两个阶段。 一是标记阶段:这个阶段内,为每个对象更新标记位,检查对象是否死亡。 二是清除阶段:对该阶段死亡的对象进行清除,执行GC操作。 | 算法简单,每个活着的对象的引用只需要找到一个,找到一个就可以判断它为活的。 | ①标记和清除效率都不高。 ②会产生大量的碎片,有的内存碎片太小无法分配给对象。 |
复制法 | 将一整块内存空间分为大小相同的两块,每次使用其中一块,当这一块用完之后就将还存活的对象复制到另一块中上面,然后再把使用过的内存空间进行一次整理。 | ①算法简单。 ②不产生内部碎片。 ③执行速度变快了,用空间换时间。 | ①内存缩小为原来一半,浪费内存空间。 ②若对象存活率较高时,效率就会变低。 |
标记整理法 | 标记清除法的改进。 同理这个算法也会把所有对象标记为存活和死亡两种状态,不同的是在第二阶段,该算法并没有直接对死亡的对象进行清理,而是把存活的对象往内存的一端移动,然后直接回收边界以外的内存,因此不会产生内存碎片。 | 提高了内存利用率,适合在收集对象存活时间较长的老生代。 | 如果存活的XX过多,移动数据时效率太低。 |
分代收集 | 新生代用复制算法。(因为新生代有很多内存需要回收,复制的内容少)老年代用标记整理或标记清除。(因为老年代仅有少量对象需要回收)。 | 执行快。 | 算法复杂。 |
⑦ 栈溢出和堆溢出现象
栈溢出: StackOverflowError
方法递归会产生,形成死循环。方法执行是创建的栈帧超过了栈的深度。
理解记忆:栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口灯信息。局部变量表又包含基本数据类型,对象引用类型(局部变量表编译器完成,运行期间不会发生变化)。
堆溢出: OutOfMemoryError :java heap space
如果不断的new对象则会导致堆中的空间溢出。
扩展:永久代溢出:OutOfMemoryError:PermGen space
由于JDK7、8移除永久带,以上溢出只发生在JDK6中。
⑧ 新生代、老生代、永久代 ★
- 新生代:存储新创建的对象。新生代又分为Eden区和两个Survivor区(S0和S1)。大部分的新创建的对象都会被分配到Eden区,当Eden区满时,会触发一次Minor GC,将Eden区中存活的对象复制到S0或S1区,同时清空Eden区和上一个Survivor区(如果有对象的话)。当S0或S1区满时,会触发一次Minor GC,将S0或S1区中存活的对象复制到另一个Survivor区,同时清空S0或S1区和上一个Survivor区(如果有对象的话)。这个过程中,如果某个对象在Survivor区中经历了多次复制,仍然存活,那么会被移动到老生代中。
- 老生代:存储长时间存活的对象。在新生代中经历了多次GC仍然存活的对象,会被移动到老生代中。老生代的内存空间通常比较大,因此GC的频率比较低。当老生代满时,会触发一次Major GC,对整个内存进行垃圾回收。
- 永久代:存储类的元数据、常量池等信息。在JDK8及以后的版本中,永久代已被删除,取而代之的是元空间,元空间是使用本地内存来存储类的元数据信息。
⑨ 垃圾回收机制
垃圾回收机制是一种自动化的内存管理技术,用于自动地回收不再使用的内存空间。在使用垃圾回收机制的编程语言中,程序员不需要手动分配和释放内存,而是由垃圾回收器自动管理内存。垃圾回收器会定期扫描内存中的对象,并标记出不再使用的对象。一旦垃圾回收器标记了不再使用的对象,就会回收这些对象所占用的内存空间。垃圾回收机制的优点是可以避免内存泄漏和野指针等问题,减少程序员的工作量,同时提高程序的安全性和稳定性。但是,垃圾回收机制也有一些缺点,如可能会导致程序的性能下降,因为垃圾回收器需要定期扫描内存中的对象,这可能会影响程序的运行速度。另外,垃圾回收机制还可能会导致内存碎片的问题,因为垃圾回收器无法保证回收的内存空间是连续的。
⑩简述堆内存中垃圾回收的划分 ★
新生代
- 新生代包含一个eden区,两个survivor区,默认比例为8:1:1。
- 新创建的对象基本都会存放在EDEN区(大对象直接放在老年代),而这部分对象大部分都会“朝生暮死”,使用后被快速回收。常规一次回收可回收70%-95%的空间,效率非常高。
- 新生代采用复制算法进行回收,假如当前正在使用的是eden及survivor1区,大部分新生对象都会放在eden区(大对象会直接放在老年代),当eden区内存满了以后,会将存活对象保存到survivor2区,eden进行Minor GC。survivor1区的存活对象根据年龄(默认是15,每经历一轮GC,年龄加1)决定去向,年龄达到阀值,复制到老年代,若年龄未到,复制到survivor2区,survivor1区清空。如果survivor2区内存不足以存放所有的存活对象,则需要以来老生代的担保机制将部分对象复制到老生代。
老生代
- 新生代与老生代默认比例为1:2,老年代用来存放存活时间较长,但还是会死的对象信息(如缓存对象、单例对象等)。
- 老年代对象来源有一下几个方向:
①大部分来自于年青代,对象在年青代存活时间过阀值,就会被复制到老年代。
②新生代中部分对象虽然未过阀值,但是因为survivor区已满,由担保机制复制到老年代。
③部分大对象直接在老年代创建,不经历年青代,如长字符串、长数组等需要大量连续空间的对象。
老年代一版采用标记-整理算法或标记-清除算法,如果采用年青代的复制算法,效
率较低。老年代对象存活时间较久,回收频率较低。
⑪简述GC回收流程 ★
①Mimor GC
首先大部分新生对象都会放在Eden区中,当Eden区内存满了以后,此时Eden区已经没有足够空间来给对象分配内存,这时会触发一次 Minor GC,把Eden区中存活的对象放到其中一个survivor区中,非存活的对象进行处理,给新创建的对象分配空间,存入到Eden区。
随着对象的分配,Eden区空间又不足了,这时候再触发一次Mimor GC,清理掉Eden区和survivor区中的死亡对象,接着把存活的对象转移到另一个survivor区中,然后再给新对象分配内存。
②Full GC当老年代中没有足够空间容纳对象时,就会触发一次Full GC,Full GC会对整个Heap进行一次GC,如果Full GC后还有无法给新创建的对象分配内存,或者无法移动那些需要进入老年代中的对象,那么JVM就会抛出OutOfMemoryError。