面经
1. Java基础知识
1.1. 为什么说Java是编译和解释的语言
Java需要把代码编译成.class的字节码文件,然后再解释成机器码。
1.2. 静态方法中为什么不能调用非静态方法
因为静态方法在类创建的时候生成,非静态方法在实例化对象之后才生成。
1.3. 重载和重写
重载:同方法名,不同参数
重写:子类重写父类的方法,方法名参数相同,构造方法无法重写
1.4. 可变长参数public void test(String …args)
可以接收不同长度的参数,重载的时候优先匹配固定长度参数的方法。
1.5. 对象实体和对象引用
new 来创建一个对象实体,一个对象实体可以有多个对象引用。一个对象引用可以指向一个对象实体。
对象实体存在堆内存中(类和对象都存放在堆内存中)
对象引用存放在栈内存中
1.6. 对象默认有无参构造,但创建有参构造后要手动创建无参构造。
1.7. 多态
子类在调用父类的方法的时候,只有在运行的时候,才知道调用的是哪个方法(父类的还是子类的),这个方法具有多态性
1.8. 接口和抽象类的区别
- 相同点:
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法,接口中可以定义default方法来实现
- 不同点
- 接口主要是对对象的约束,约束对象的行为(实现接口的方法)
- 抽象类主要是代码的复用性的规定
- 一个类只可以继承一个类,但可以实现多个接口
- 接口中 的成员变量只能是public static final 类型,有初值,不可变
- 抽象类中的成员变量默认为default,可在子类中重新定义和赋值
1.9. 深拷贝和浅拷贝
浅拷贝:在对上创建一个新对象,如果被拷贝的是引用类型,则会直接复制它引用的地址
深拷贝:复制整个对象,包含内部对象
1.10. Object 的常用方法
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
hashcode 和 equals 都是判断对象是否相同的方法
hashcode 可能会碰撞,equals 是比较地址但效率较低
重写equals也应当重写hashcode方法。
1.11. String StringBuffer StringBuilder 的区别
String 对象不可变,String a = “test”; a = “test1” 。更改a的值的时候,是创建了一个新的String对象,将地址赋给a。是线程安全的
StringBuffer 和 StringBuilder 更改值,不用新建对象。
StringBuffer 使用同步锁保证线程安全
StringBuilder 是线程不安全的,效率比StringBUffer快10%-15%
1.12. String拼接、赋值和intern方法
- 在Java9之前,String 用加号连接的方法是使用StringBuilder.append().toString()方法构建的。在之后更新了方法,可以放心使用+连接,性能也不错
- String s = new String(“abc”); 这段代码可能会创建 1-2 个字符串对象。 先在字符串常量池创建一个字符串对象abc(如果已存在则不需要再创建),然后复制一份到堆内存中。
- String intern 方法:将引用保存到字符串常量池中(如果已有则不再保存),并且返回这个对象
1.13. 字符串常量池
- 字符串常量池位于堆内存中,与堆中的对象实例是分开的。
- 字符串常量池中的字符串是不可变的,一旦创建就不能修改。
- 字符串常量池中的字符串可以通过调用 intern() 方法进行显式的添加。该方法将返回常量池中字符串的引用,如果常量池中已存在相同内容的字符串,则返回已存在的引用。
- 字符串常量池的位置发生了变化,在 Java 7 及之前的版本中,字符串常量池位于永久代(PermGen),而在 Java 8 及以后的版本中,它被转移到了堆内存中。
2. 集合
2.1. List
顺序存储
ArrayList
- ArrayList和Array的区别
ArrayList | Array | |
---|---|---|
长度 | 可变 | 固定 |
用泛型 | 可 | 不可 |
存放类型 | 仅对象 | 对象和基本类型 |
内置方法 | 有 | 无 |
创建指定大小 | 无需 | 必须 |
存放null | 可 | 不可 |
ArrayList和LinkedList操作的时间复杂度。
ArrayList | LinkedList | |
---|---|---|
头插 | O(n) | O(1) |
尾插 | O(1)或O(n) (扩容) | O(1) |
中间插入 | O(n)后面的要向后移动 | O(n)找到插入的位置 |
头删 | O(n) | O(1) |
尾删 | O(1) | O(1) |
中间删除 | O(n) | O(n) |
Vector:
Vector
是List
的古老实现类,底层使用Object[]
存储,线程安全。LinkedList
2.2. Set
2.2.1. 类别:
- HashSet:无序,唯一,使用HashMap实现
- LinkedHashSet:使用LinkedHashMap实现
- TreeSet:有序,唯一 也叫:红黑树 (自平衡的排序二叉树)
2.2.2. Set排序接口
- Comparable:实现方法obj.compareTo(obj1);
- Comparator:实现方法compare(obj1,obj2);
2.2.3. 无序性和不可重复性
无序性:存储元素的顺序是按照key散列之后存储在对应位置,不是按顺序一个一个存储
不可重复性:equals不能相同,同时需要重写hashcode和equals方法
2.3. Queue
2.3.1. 类型
- PriorityQueue:数组实现的二叉堆
- ArrayQueue:数组和双指针实现
2.3.2. Queue和Deque的区别
Queue是单端队列,Deque是双端队列
Queue实现了Collection接口
Deque扩展了Queue接口,增加了队首删除和加入的方法,根据错误处理方式有两种不同操作方法
Deque 接口 |
抛出异常 | 返回特殊值 |
---|---|---|
插入队首 | addFirst(E e) | offerFirst(E e) |
插入队尾 | addLast(E e) | offerLast(E e) |
删除队首 | removeFirst() | pollFirst() |
删除队尾 | removeLast() | pollLast() |
查询队首元素 | getFirst() | peekFirst() |
查询队尾元素 | getLast() | peekLast() |
事实上,Deque
还提供有 push()
和 pop()
等其他方法来模拟栈
2.3.3. ArrayDeque和LinkedList的区别
ArrayQueue | LinkedList | |
---|---|---|
底层 | 数组+双指针 | 链表 |
可存储null | 否 | 是 |
需要扩容 | 可能需要,但插入均摊性能高 | 否,但插入元素需要申请内存,均摊性能稍低 |
ArrayQueue的性能比LinkedList好,并且可以实现栈。
2.3.4. PriorityQueue
PriorityQueue和Queue的区别是:PriorityQueue是根据优先级出队的
细节TODO
2.3.5. BlockingQueue(了解)
没有元素会阻塞,直到有元素。如果队列已满,会阻塞插入操作。
用来实现生产者消费者等部分。
2.4. Map
2.4.1. 类别
- HashMap:不保证FIFO,不是先来先存储,通过散列,选择一个位置存放
- LinkedHashMap:保证FIFO
- HashTable:
- TreeMap:
2.4.2. HashMap:
[hashMap](# HashMap源码)
2.4.3. HashTable(接近淘汰):
线程安全,效率稍低,不支持存储null或nullkey,容量默认为11,之后扩容为2n+1
2.4.4. HashMap和HashSet的区别
HashSet是HashMap实现的
HashSet
的源码非常非常少,除了 clone()
、writeObject()
、readObject()
是 HashSet
自己实现之外,其他方法都是直接调用 HashMap
中的方法。
HashMap |
HashSet |
---|---|
实现了 Map 接口 |
实现 Set 接口 |
存储键值对 | 仅存储对象 |
调用 put() 向 map 中添加元素 |
调用 add() 方法向 Set 中添加元素 |
HashMap 使用键(Key)计算 hashcode |
HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals() 方法用来判断对象的相等性 |
- HashSet的去重:调用HashMap的add方法,根据返回值判断是否重复
- HashMap的判断重复:计算hash值->相同equals->再相同则重复
2.4.5. TreeMap
实现 NavigableMap
接口让 TreeMap
有了对集合内元素的搜索的能力。
实现SortedMap
接口让 TreeMap
有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序
想要重写排序
TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
@Override
public int compare(Person person1, Person person2) {
int num = person1.getAge() - person2.getAge();
return Integer.compare(num, 0);
}
});
比HashMap多了元素根据键排序和对集合内元素的搜索能力
2.5. Collections工具类方法
//排序
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
//查找 替换
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target)
boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素
同步操作(不推荐,建议使用JUC)
2.6. 集合的建议
判空使用
isEmpty()
方法在使用
java.util.stream.Collectors
类的toMap()
方法转为Map
集合时,一定要注意当 value 为 null 时会抛 NPE 异常。不要在 foreach 循环里进行元素的
remove/add
操作。remove 元素请使用Iterator
方式,如果并发操作,需要对Iterator
对象加锁。Collection#removeIf()
方法删除满足特定条件的元素可以利用
Set
元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List
的contains()
进行遍历去重或者判断包含操作。使用集合转数组的方法,必须使用集合的
toArray(T[] array)
,传入的是类型完全一致、长度为 0 的空数组。使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法, 它的add/remove/clear
方法会抛出UnsupportedOperationException
异常。推荐解决方法
Integer [] myArray = { 1, 2, 3 }; List myList = Arrays.stream(myArray).collect(Collectors.toList()); //基本类型也可以实现转换(依赖boxed的装箱操作) int [] myArray2 = { 1, 2, 3 }; List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
最简单的解决办法
Integer [] myArray = { 1, 2, 3 }; List myList = Arrays.stream(myArray).collect(Collectors.toList()); //基本类型也可以实现转换(依赖boxed的装箱操作) int [] myArray2 = { 1, 2, 3 }; List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
3. 常用数据结构源码解析
3.1. HashMap源码
底层是数组+链表+红黑树,非线程安全
- 为什么非线程安全
线程一进行完hash碰撞判断之后,时间片用尽,线程二完成了hash计算和插入操作,此时线程一直接插入,会覆盖线程二插入的值
同时put导致size值不对
- 为什么非线程安全
可存储null的key和value
初始大小16,扩容到当前的二倍(一定是2的幂次方,便于取余计算)
获取Key的方法:
static final int hash(Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^:按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
元素添加:
- 获取数组的index:key对hashmap的length取模(将key和length-1与,比%取模的性能好)
- 如果这个index没有元素,直接添加
- 如果有元素则比较key,如果key相同则覆盖。
- 如果是hash碰撞,则判断是否是树节点,是就调用putTreeVal加入树节点,否则加入链表尾部
hash碰撞使用拉链法
- 在hash碰撞的节点创建一个链表,把碰撞的值都放在链表中
- 链表元素超过8个,将链表切换成红黑树
扩容条件是存放的元素数量超过
容量*负载因子
- 负载因子loadFactor 是控制数组存放数据的疏密程度
- loadFactor越趋近1,数组中存储的元素就越多,碰撞的元素在链表的长度就越多,查询性能下降
- loadFactor越趋近0,hashMap扩容次数增加,rehash消耗性能
- 官方给出的默认为0.75
- resize就是扩容之后重新计算index和hash
- 底层就是新开一个数组,将元素重新放入新数组
- 负载因子loadFactor 是控制数组存放数据的疏密程度
3.2. ArrayList
ArrayList
继承于AbstractList
,实现了List
,RandomAccess
,Cloneable
,java.io.Serializable
这些接口。线程不安全。List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。RandomAccess
:表明它可以快速进行随机访问,get(index)Cloneable
:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
可以添加null值。
底层是Object数组。
时间复杂度:add() O(1) ; add(index,val) O(n) ;
空间占用主要是末尾预留的空间。
在添加大量元素的时候,可以提前使用ensureCapacity(N)方法预留空间,减少空间分配次数,节约性能。
当添加元素大于容量的时候,触发扩容
- 扩容:每次newSize= oldSize+oldSize/2 相当于1.5倍
3.3. LinkedList
LinkedList
继承了AbstractSequentialList
,而AbstractSequentialList
又继承于AbstractList
。LinkedList
实现了List
,Deque
,Cloneable
,Seralizable
Deque
:表明它具有双端队列特性,便于两端插入和删除
- 底层是双向链表
- 时间复杂度:add() O(1); add(index,val) O(n)
- 空间占用主要是除了data之外的索引等数据
- 遍历常用for-each
- 基本上不用LinkedList,都用ArrayList,性能一般更好一些
3.4. ConcurrentHashMap
3.4.1. 实现线程安全的方法:
- JDK1.7的时候:使用Segment数组(锁的个数,默认16,不可扩容)+HashEntry数组+链表实现。使用分段锁,将数据分段保护。最多支持16个线程并发(默认)
- JDK1.8的时候:使用(node数组+链表)/(红黑树)实现。主要使用CAS和synchronized操作,保留一些分段锁用来兼容旧版本。synchronized锁定链表节点或者红黑树首节点。因此只要hash不碰撞都可以进行并发操作。
3.4.2. 不允许存放null和nullkey
避免二义性,不同线程使用containsKey来判断是否存在元素,如果存储null就不知道到底是有还是没有值。
3.4.3. 不能保证复合操作的原子性
// 线程 A
if (!map.containsKey(key)) {
map.put(key, value);
}
// 线程 B
if (!map.containsKey(key)) {
map.put(key, anotherValue);
}
不能保证上面的代码正常执行
concurrentHashMap提供了原子性的符合操作方法:putIfAbsent()
或computeIfAbsent()
4. 并发编程
4.1. synchronized关键字的底层原理是什么
2.1.1synchronized是做什么的
给线程加锁,加锁目标是一个类或一个对象。
2.1.2实现原理
加锁指令:monitorenter(加锁) monitorexit(释放锁)
一个对象或类关联有一个monitor(计数器:正在使用的线程数,类似信号量)
4.2. CAS的理解和底层实现原理
多个线程要访问同一个数据会出现并发安全问题。
AtomicInterger
并发包的原子类,使用CAS实现。
4.2.1. 线程操作AtomicInterger基本流程:
线程1想要修改值,会
- 先读取旧值
- 在修改前再次读取这个值
- 如果没人修改,则使用CAS进行修改这个值。
- 如果第二次读取的值和旧值不同,则CAS失败。
4.2.2. CAS
CAS:compare and set
在硬件级别保是原子操作,同一时间只有一个线程可以执行CAS。
ABA问题:线程1读取值A,线程2读取值A修改成B之后再修改成A,这样线程1不知道是不是被修改了。
解决:加上时间戳(版本号)
4.3. JDK中的AQS的实现原理
ReentrantLock
类底层是AQS(Abstract Queue Synchronizer)
可以使用这个类生成一个锁lock,可以进行lock.lock() lock.unlock()实现互斥。
2.4.1 AQS会有一个等待队列,存储没有得到锁的线程,待锁释放后,按顺序为等待队列的线程提供锁
2.4.2如果在线程1执行完毕,唤醒线程2的过程中,如果有线程3想要加锁
- 非公平锁:ReentrantLock lock = new ReentrantLock();
- 线程3可能会成功得到锁,达到插队。
- 公平锁:ReentrantLock lock = new ReentrantLock(true);
- 如果等待队列有线程,线程3会进入等待队列。
4.4. 线程池的底层工作原理
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。强制线程池不允许使用 Executors
去创建,而是通过 ThreadPoolExecutor
构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险(阿里巴巴Java规范)
4.4.1. 线程池:
提供一定量的线程,线程执行完成任务后,不销毁自己,等待下一次任务:
避免重复创建和销毁线程,造成性能浪费
创建线程池:
ExecutorService threadPool = Executor.newFixedThreadPool(10) //(corePoolSize = 10)
threadPool.submit(new Callable() {
public void run(){}
});
有新任务的时候
- 如果线程池的线程数量小于容量,则直接创建一个新的线程执行任务。
- 如果满了,则放在任务队列中。
当线程完成自己的任务的时候,会去任务队列中获取任务,如果没有任务,会阻塞,不会销毁。
4.4.2. 线程池的核心配置参数
代表线程池的类是ThreadPoolExecutor
return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
corePoolSize:3
- 一般最大线程数只有3
maximumPoolSize:200
- 当任务队列满了,可以最多额外创建到200个,执行任务并从任务队列获取任务
keepAliveTime:60s
- 任务队列空了60s后,额外线程自动会销毁掉
new ArrayBlockingQueue<Runnable>(200)
- 任务队列
如果额外线程也满了,会报异常。可以自定义RejectedExecutionHandler策略来应对这种情况:持久化被reject的任务,等负载低了再加载执行。
4.4.3. 如果在线程池中使用无界阻塞队列会发生什么问题
面试题1:如果使用无界阻塞队列调用远程服务,远程服务异常,会不会导致内存异常飙升
调用超时,队列变得越来越大,内存会飙升,可能会导致OOM。
4.4.4. 线程池队列满了之后,会发生什么
- 如果给maximumPoolSize设置太大,可能会导致系统崩溃。因为线程会占用一定内存,也会增加cpu负载。
- 如果给maximumPoolSize设置太小,可能会导致任务reject。
4.4.5. 如果机器突然宕机,线程池的阻塞队列的任务怎么办
都会丢失
解决办法:在数据库对任务信息进行持久化。
4.5. ThreadLocal
主要解决的问题:让每个线程绑定自己的值,防止竞争
最终的变量是放在了当前线程的 ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。 ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。
每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
4.5.1. 内存泄漏问题
ThreadLocalMap 使用的key是对ThreadLocal的弱引用,value是强引用。垃圾回收的时候会回收弱引用对象,那么key可能会被回收,导致Map的key为null。如果我们不操作,value则无法被回收
ThreadLocalMap提供的方法:set get remove 都会清楚key为null 的记录。用完ThreadLocal之后最好进行remove()
4.6. Future类
异步思想的典型运用。
耗时的任务交给future类实现,先执行其他步骤,等到我们需要的时候再通过future类获取。
// V 代表了Future执行的任务返回值的类型
public interface Future<V> {
// 取消任务执行
// 成功取消返回 true,否则返回 false
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否被取消
boolean isCancelled();
// 判断任务是否已经执行完成
boolean isDone();
// 获取任务执行结果
V get() throws InterruptedException, ExecutionException;
// 指定时间内没有返回计算结果就抛出 TimeOutException 异常
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutExceptio
}
5. Java IO
InputStream/Reader:输入流基类,前者是字节输入流,后者是字符输入流
OutputStream/Writer:输出流基类,前者是字节输出流,后者是字符输出流
5.1. InputStream
用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream
抽象类是所有字节输入流的父类。
5.1.1. 常用方法
read()
:返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回-1
,表示文件结束。read(byte b[ ])
: 从输入流中读取一些字节存储到数组b
中。如果数组b
的长度为零,则不读取。如果没有可用字节读取,返回-1
。如果有可用字节读取,则最多读取的字节数最多等于b.length
, 返回读取的字节数。这个方法等价于read(b, 0, b.length)
。read(byte b[], int off, int len)
:在read(byte b[ ])
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字节数)。skip(long n)
:忽略输入流中的 n 个字节 ,返回实际忽略的字节数。available()
:返回输入流中可以读取的字节数。close()
:关闭输入流释放相关的系统资源。
JDK1.9之后添加了:
readAllBytes()
:读取输入流中的所有字节,返回字节数组。(实用)readNBytes(byte[] b, int off, int len)
:阻塞直到读取len
个字节。transferTo(OutputStream out)
:将所有字节从一个输入流传递到一个输出流。
5.1.2. FileInputStream
FileInputStream
是一个比较常用的字节输入流对象,可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中。
try (InputStream fis = new FileInputStream("input.txt")) {
System.out.println("Number of remaining bytes:"
+ fis.available());
int content;
long skip = fis.skip(2);
System.out.println("The actual number of bytes skipped:" + skip);
System.out.print("The content read from file:");
while ((content = fis.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
一般我们是不会直接单独使用 FileInputStream
,通常会配合 BufferedInputStream
(字节缓冲输入流,后文会讲到)来使用。
// 新建一个 BufferedInputStream 对象
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
// 读取文件的内容并复制到 String 对象中
String result = new String(bufferedInputStream.readAllBytes());
System.out.println(result);
5.1.3. DataInputStream
DataInputStream
用于读取指定类型数据,不能单独使用,必须结合其它流,比如 FileInputStream
。
FileInputStream fileInputStream = new FileInputStream("input.txt");
//必须将fileInputStream作为构造参数才能使用
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
//可以读取任意具体的类型数据
dataInputStream.readBoolean();
dataInputStream.readInt();
dataInputStream.readUTF();
5.1.4. ObjectInputStream
ObjectInputStream
用于从输入流中读取 Java 对象(反序列化),ObjectOutputStream
用于将对象写入到输出流(序列化)。
ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data"));
MyClass object = (MyClass) input.readObject();
input.close();
另外,用于序列化和反序列化的类必须实现 Serializable
接口,对象中如果有属性不想被序列化,使用 transient
修饰。
5.2. OutPutStream
OutputStream
用于将数据(字节信息)写入到目的地(通常是文件),java.io.OutputStream
抽象类是所有字节输出流的父类
5.2.1. 常用方法
write(int b)
:将特定字节写入输出流。write(byte b[ ])
: 将数组b
写入到输出流,等价于write(b, 0, b.length)
。write(byte[] b, int off, int len)
: 在write(byte b[ ])
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字节数)。flush()
:刷新此输出流并强制写出所有缓冲的输出字节。close()
:关闭输出流释放相关的系统资源。
5.2.2. FileOutputStream
FileOutputStream
是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。
try (FileOutputStream output = new FileOutputStream("output.txt")) {
byte[] array = "JavaGuide".getBytes();
output.write(array);
} catch (IOException e) {
e.printStackTrace();
}
类似于 FileInputStream
,FileOutputStream
通常也会配合 BufferedOutputStream
FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream)
5.2.3. DataOutputStream
DataOutputStream
用于写入指定类型数据,不能单独使用,必须结合其它流,比如 FileOutputStream
。
// 输出流
FileOutputStream fileOutputStream = new FileOutputStream("out.txt");
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
// 输出任意数据类型
dataOutputStream.writeBoolean(true);
dataOutputStream.writeByte(1);
5.2.4. ObjectInputStream
ObjectInputStream
用于从输入流中读取 Java 对象(ObjectInputStream
,反序列化),ObjectOutputStream
将对象写入到输出流(ObjectOutputStream
,序列化)
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("file.txt")
Person person = new Person("Guide哥", "JavaGuide作者");
output.writeObject(person);
5.3. Reader
Reader
用于从源头(通常是文件)读取数据(字符信息)到内存中,java.io.Reader
抽象类是所有字符输入流的父类
Reader
用于读取文本, InputStream
用于读取原始字节。
5.3.1. 常用方法
read()
: 从输入流读取一个字符。read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组cbuf
中,等价于read(cbuf, 0, cbuf.length)
。read(char[] cbuf, int off, int len)
:在read(char[] cbuf)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。skip(long n)
:忽略输入流中的 n 个字符 ,返回实际忽略的字符数。close()
: 关闭输入流并释放相关的系统资源。
5.3.2. InputStreamReader FileReader
InputStreamReader
是字节流转换为字符流的桥梁,其子类 FileReader
是基于该基础上的封装,可以直接操作字符文件。
try (FileReader fileReader = new FileReader("input.txt");) {
int content;
long skip = fileReader.skip(3);
System.out.println("The actual number of bytes skipped:" + skip);
System.out.print("The content read from file:");
while ((content = fileReader.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
5.4. Writer
Writer
用于将数据(字符信息)写入到目的地(通常是文件),java.io.Writer
抽象类是所有字符输出流的父类。
5.4.1. 常用方法
write(int c)
: 写入单个字符。write(char[] cbuf)
:写入字符数组cbuf
,等价于write(cbuf, 0, cbuf.length)
。write(char[] cbuf, int off, int len)
:在write(char[] cbuf)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。write(String str)
:写入字符串,等价于write(str, 0, str.length())
。write(String str, int off, int len)
:在write(String str)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。append(CharSequence csq)
:将指定的字符序列附加到指定的Writer
对象并返回该Writer
对象。append(char c)
:将指定的字符附加到指定的Writer
对象并返回该Writer
对象。flush()
:刷新此输出流并强制写出所有缓冲的输出字符。close()
:关闭输出流释放相关的系统资源。
5.4.2. OutputStreamWriter FileWriter
OutputStreamWriter
是字符流转换为字节流的桥梁,其子类 FileWriter
是基于该基础上的封装,可以直接将字符写入到文件。
try (Writer output = new FileWriter("output.txt")) {
output.write("你好,我是Guide。");
} catch (IOException e) {
e.printStackTrace();
}
5.5. 字符缓冲流BufferedInputStream BufferedOutputStream
IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。
默认大小8M,可显式定义
5.6. 打印流 PrintStream
System.out就是一个PrintStream对象,print调用了write方法
PrintStream
属于字节打印流,与之对应的是 PrintWriter
(字符打印流)。PrintStream
是 OutputStream
的子类,PrintWriter
是 Writer
的子类。
5.7. 随机访问流 RandomAccessFile
可以访问到文件的任意字节。
适用于断点重传。
6. 谈谈对Java内存模型的理解
public class HelloWorld {
private int data;
public void increment(){
data++;
}
}
HelloWorld helloWorld = new HelloWorld(); //对象存放在堆内存,包含对象中的实例变量
//线程1
new Thread(){
public void run(){
helloWorld.increment();
}
}.start()
//线程2
new Thread(){
public void run(){
helloWorld.increment();
}
}.start()
常量:主存(内存)
线程的工作内存:cpu缓存
常量操作:read load use assign store write
6.1. 可见性、原子性、有序性
- 可见性
- 没有可见性:线程1更新了数据,但是线程2看到的还是工作内存中旧的数据
- 有可见性:数据更新之后,线程1会强制使线程2重新读取修改后的数据。
- 原子性
- 一次只有一个线程进入临界区。data++必须是独立执行的。
- 有序性
- 在任务需要的资源准备完全之后,执行该线程任务。
6.2. 从底层角度聊volatile关键字原理
volatile:用来解决可见性和有序性,对原子性的保证很有限。(对64位的long型有一定原子性保证)
- 实现可见性
- 当加上volatile的变量改变时,会使其他线程工作内存的过期变量失效。
- 保证有序性
- 保证写在读之前
6.3. 指令重排和happens-before原则
指令重排有可能导致有序性失效。
happens-before原则:
- 线程内按照代码顺序,写在前面的代码先行发生在卸载后面的代码。
- 锁定操作:对锁的unlock操作先行发生在lock操作
- volatile变量原则:写操作在读操作之前
- 传递原则:A先于B,B先于C,则A先于C
- 线程启动原则:线程的启动thread.star()先于线程中的其他操作。还有interrupt
- 线程终结原则:线程的所有操作都先于线程的终止检测,使用thread.jion()结束。
- 对象终结原则:一个对象的初始化完成在finalize()方法之前。
6.4. volatile底层如何基于内存屏障保证可见性和有序性
对volatile的值的操作代码前后加上内存屏障。
内存屏障:禁止重排序
7. Spring
7.1. Spring的IOC
如果没有IOC:tomcat+servlet:tomcat 监听端口来将请求转发给servlet来处理,耦合严重,需要变动时,修改很麻烦。
IOC:依赖注入,控制反转,容器根据xml配置或者注解来对bean对象之间的引用关系进行依赖注入
底层核心技术:反射。根据类来自动构建对应的对象。
类与类彻底解耦。
7.2. Spring的AOP
MySQL:事务:一次开启一个事务,其中进行多次增删改查。如果有一条失败了,会回滚事务,把这个事务中所有的sql语句都恢复。
AOP:做一个切面Aspect,给所有类似servicexxx代码之前都会开启一个事务,在这些方法运行完毕之后,根据是否抛出异常,去回滚或者提交事务。
核心技术:动态代理
Spring会给正在运行的类生成动态代理类,包含我们写的类。然后在代理类中给逻辑前后加上事务。
如何限定AOP。
TODO
7.3. 了解过cglib动态代理吗,他和jdk动态代理的区别是什么
jdk动态代理,有接口的时候使用,生成一个实现这些同样接口的对象。
没有接口会使用cglib来生成你的类的子类,覆盖你的类的方法,在方法中加入增强的代码。
7.4. spring事务的实现原理是什么,事务传播机制是什么
不同事务之间不互相影响
@Transactional(propagation = Propagation.REQUIRED)会开启一个事务
- Propagation_REQUIRED:如果当前没有事务,创建一个事务,多个调用加入到一个事务中。
- Propagation_SUPPORT:之前有事务则加入,没有则不开启。
- Propagation_MANDATORY:有事务加入,没有则报错。
- Propagation_RESQUIRES_NEW:强制开启一个新事务。
- Propagation_NOT_SUPPORTED:不使用事务,有事务会挂起
- Propagation_NEVER:不允许使用事务,有事务会报错
- Propagation_NESTED:嵌套事务,外层事务回滚会导致内存事务也回滚,内层不影响外层。
7.5. Springboot 的核心架构
自动装配依赖。不需要像spring一样自己配置xml文件,引入jar包。减少了配置。
7.6. Spring 核心源码
Spring bean 生命周期:
- 创建bean
- 实例化一个bean
- 依赖注入
- 把这个bean的依赖的bean实例化,也进行依赖注入。注入方法:构造函数,setter方法。
- 处理Aware接口
- 如果这个bean实现了Aware相关的接口,Spring容器会把自己的信息注入给bean中。
- BeanPostProcesser
- 在bean实例初始化之前和之后可以执行的方法。
- init初始化方法
- 销毁
- DisposableBean接口,会调用这个接口实现的destroy方法
- 最后,如果配置了destroy-method方法,会调用这个方法
7.7. Spring中的设计模式
工厂,单例,代理
工厂模式:使用工厂类来创建类。
单例模式:每个bean在系统运行期间只会创建一个实例对象。
代理模式:AOP
7.8. SpringMVC架构
- tomcat 监听端口,将请求转发给SpringMVC的DispathcherServlet
- 然后SpringMVC再根据url将请求转发给对应的controller
- 返回json给前端,前端符合渲染
7.9. SpringCloud核心架构
这些框架
8. JVM
8.1. JVM中有哪几块内存区域,Java8之后对内存分代做了什么改进
- 栈内存:每个线程独有
- 堆内存:存放对象、实例
- 永久代区域:我们写的类
Java8以后永久代变成metaspace
常量区放在了堆里面
8.2. JVM如何运行起来的,如何创建各种对象
线程执行main函数同时创建对象
Spring容器创建一些bean对象
把执行的方法和局部变量放在栈帧。
8.3. JVM什么时候会触发垃圾回收
内存分代:年轻代(eden:s1: s2, 8:1:1 )和老年代
年轻代和老年代统称为堆
新生成的对象实例存放在年轻代。
- ygc:Eden区满了。进行youngGC
- 没有引用的对象(类)被回收
年轻代垃圾回收算法:
- 复制算法:因为年轻代中大多数都是垃圾对象,所以把存活对象复制到s1中,一键全部清除Eden区。
- 把s1和Eden中的存活对象复制到s2,把s1和Eden区全部清除
8.4. 什么时候对象会转移到老年代
如果存活了多次垃圾回收过程,就会转移到老年代
如果s区放不下,会把一些存活的对象直接放到老年代中。
对于大对象会直接放到老年代中,防止ygc反复复制大对象。
8.5. 常用的垃圾回收器,老年代如何回收
老年代中大多数是长期存活的对象,所以使用标记-清理方法:把所有存活的对象压缩到连续的位置,然后统一清理,可以防止内存碎片问题。
常用的垃圾回收器:
- CMS+parnew jdk8-jdk9
- g1 jdk11
- ZGC
8.6. 生产环境如何设置jvm参数的,如何检查jvm的运行情况
tomcat的配置脚本,catalina脚本设置。
如果使用jar启动,再java命令后直接加上参数
参数:
- 内存区域大小的分配:
- 栈大小
- metaspace大小
- eden survivor
- 堆大小
- 年轻代、老年代
- 垃圾回收器
- 年轻代和老年代用了什么回收器
- 是否有特殊参数,作用是什么
jstat压测,QPS,接口性能
8.7. JVM GC优化
自己动手进行压测,调试一下
8.8. 发生OOM之后,应该如何排查和处理线上系统的OOM问题
在jvm设置参数,发生oom之后保存快照。
找出占用内存最大的对象和创建它的代码,进行调优。
9. 网络
9.1. TCP/IP的四层模型和七层网络模型
TCP/IP四层:数据链路层、网络层、传输层、应用层
- 物理层:硬件部分
- 数据链路层:将0/1信号分组,确定来源去向
- 以太网协议:一组信号是一个网络帧。每帧有两个部分:表头和数据,表头保存说明性的东西,比如发送者,接收者,数据类型等。通过网卡来发送接收数据,mac地址是网卡的id。
- mac:前6个16进制是厂商编号,后6个编号是网卡流水号。
- 网络层:
- IP协议
- 判断是不是一个子网:使用ip的二进制和子网掩码进行与运算,看结果前三个部分如果是一样的就是子网。
- 不在一个子网需要一个路由器,路由器判断数据包的目标mac是不是自己的子网内的mac,是则转发。
- ARP cache 会让每一个电脑都缓存到子网中所以电脑的ip和mac对应关系。
- 路由器就可以看做是一个网关
- 传输层TCP协议:仅仅规定了基于端口的点对点通信协议,包含如何建立连接,读取和发送信息。要基于TCP 开发,实际上是使用socket开发。
- 应用层:最常见的是http
OSI七层:物理层、会话层、表示层 + 四层模型
DNS:domain name server ,先通过dns服务器把域名翻译成IP。
9.2. 浏览器访问baidu.com会发生什么
- 域名解析为IP
- 把请求打包成http包
- 把http数据包包装成tcp数据包,tcp数据头包含接收者和发送者的端口号
- 然后把全部数据包包装到ip数据包,ip数据头中包含发送者和接收者的ip
- 然后以太网会把这个数据包封装到以太网数据包中,加上以太网头,其中包含有发送者和接收者的网卡mac地址。
- 以太网一次传输字节有限,可能需要切割为多个包。
- 根据IP头序号来合成一个包。
- 然后通过多个路由转发到百度的子网中。
9.3. TCP三次握手和四次挥手的流程,为什么不是五次或者两次?
- 建立连接的三次握手:
- 客户端发送syn(同步),表示自己进入了syn_send状态
- 服务端恢复syn+ack(确认)表示确认收到同步请求,并且自己进入syn_recevie状态
- 客户端发送ack,表示确认建立连接。当服务端接收到这个包的时候,连接正式建立
- 为什么不是两次握手
- 如果网络问题,遇到不想要的连接,三次连接可以让客户端发送给服务器复位信息,释放资源
- 结束连接的四次挥手:
- 客户端发送FIN(结束)
- 服务端发送ACK(收到),这段时间有可能传输还没有完全完毕,等待全部完毕后再发送FIN。
- 服务端发送FIN(结束)
- 客户端发送ACK(收到):服务端收到这个请求后立刻关闭,客户端会等待一段时间,保证服务端确实接收到这个包
2 3次挥手好像在某些情况可以合并?
9.4. 说一下http长连接的原理
http本身没有长连接,都是tcp的长连接和短链接
http协议规范:请求头,请求体什么的
http1.0 都是短链接,一次请求后直接断开tcp连接,需要指定keep-alive才能建立长连接
http1.1 默认是长连接
http2.0支持多路复用,一个tcp可以并行发送多个请求以及接收响应。
http3.0 QUIC 建立在udp之上。
9.5. https http+ssl/tsl
使用证书加密
- 非对称加密:rsa
- 网站给浏览器发送证书(由权威机构颁发),浏览器验证合法性
- 浏览器生成随机密码,用随机密码加密随机密码的hash,并且用证书的公钥加密这个随机密码,
- 网站用证书的私钥解密这个随机面膜,再用随机密码解密得到hash,计算自己得到的密码的hash进行对比,如果完全相同,则可以使用
- 之后用这个随机密码来实现加密通信。
10. MySQL
10.1. 引擎:mysiam innodb
- mysiam:不支持事务,不支持外键约束。索引和数据文件分开,可以在内存缓存更多索引,查询性能会更好,适用于少量插入,大量查询
- hadoop报表系统,用mysql mysiam比较适合,但是数据量太大超过500w以上就也不能用mysql了。
- innodb(默认):支持事务,外键约束,高并发,高可用,大数据量
10.2. Mysql索引原理和数据结构。
索引:默认b+树
- b-树
- 每个节点都存储对应的data
- b+树
- 只有叶子节点存储对应的data
Mysiam的索引:叶子节点存储的是索引的物理地址。然后用物理地址去数据文件找数据。
Innodb的索引:表要求必须要有主键,默认会为主键建立一个索引,节点data包含所有数据(一个记录,整行),叫做聚簇索引。如果你使用name来找数据,那么从name索引中找到的data是主键(id),再用id从聚簇索引找data。
10.3. 索引的使用规则
怎么建立索引?
最左前缀匹配原则:
创建联合索引:create index(shop_id,product_id,gmt_create)
如果你使用 shop_id 和 gmt_create来查找,那么不会直接通过这个联合索引查找,而是通过使用shop_id筛选出来一些数据,之后扫描gmt_create字段符合要求过滤。(性能也还行)。
但如果没有最左边的任何字段,就没法用这个索引,比如直接通过product_id查找,这个是没有用到这个索引的。
范围列匹配,最左前缀范围查找会用索引,之后的不会用索引了。
调用了函数的sql语句不使用索引
建立尽量少的索引,10条以内为佳
尽量选唯一字段进行建立索引。选择的字段 去重后数量/总数量 ,结果要是小,则说明这个索引用处不大。
10.4. 事务的几个特点
ACID
- Atomic:原子性,同时执行的sql要么一起成功,要么一起失败
- Consistency:一致性,事务之前前后数据都应该是正确的
- Isolation:隔离性,多个事务之间不互相干扰
- Durability:持久性
10.5. 隔离级别
- 读未提交:事务A读取到事务B还没有提交的数据
- 读已提交:事务A读取到原来的数据,然后事务B提交修改,事务A再次读取,读到不一样的数据。(不可重复读)
- 可重复读:事务A读取到原来的数据,然后事务B提交修改,事务A再次读取,读到的还是事务A最开始读取到的数据。(可重复读)
- 幻读(不是隔离级别):事务A查询所有数据,准备插入一一条数据,事务B插入了一行数据,事务B提交插入。事务A想要插入数据,发现这个数据已经存在(被事务B插入)。
- 串行化(为了解决幻读):事务A查询所有数据,事务B想要插入一行数据会被拒绝。事务A提交后,事务B才能进行插入数据。
MySQL默认级别:可重复读。
实现可重复读的机制:MVCC机制 multi-version concurrency control
事务id是全局唯一且递增的,
查询事务只会找比自己事务id小的 创建事务。
创建事务id<=当前事务id
当前事务id<删除事务id
不同事务修改某行数据,会多出来一行,id相同。
10.6. 数据库锁
自动加锁
表锁 行锁 页锁
- myisam会加表锁。锁表的时候查询会报504
行锁:innodb:共享锁(s)和排他锁(x)
- 共享锁和排他锁不能同时加
- select 不加锁因为MVCC有快照,增删改会加一个行锁排他锁。
手动加锁
加共享锁:select * from table where id = 1 lock in share mode
加排他锁:select * from table where id =1 for update
悲观锁:进行操作都加上排他锁
乐观锁:加上版本号字段,在事务修改期间如果版本号不同,则这次修改失败,需要重新读取操作。
死锁:dba查看死锁日志。
10.7. MySQL调优的常用手段
- 保持sql简单,建议使用单表查询
- 优化索引
- 查看sql的执行计划:explain select * from table
10.8. E-R图
entities-relationship图
关系:操作数是关系,重复行的对应关系(投影)要去掉。剩下的是关系:一对多,一对一,多对多等
11. socket
直接使用tcp进行通信,就是socket编程
可以认为socket处于传输层。或者是介于传输层和应用层直接。
socket 就是封装了tcp的编程规范
12. 进程通信和线程切换
12.1. 进程通信
9.1.1管道 pipe
只有父子进程(fork得到的)才能使用这个管道进行通信。
9.1.2命名管道
无亲缘关系的管道可以使用命名管道通信
9.1.3消息队列
9.1.4共享内存
12.2. 线程如何切换
时间片轮换
优先级调度等
13. nio,bio,aio都是什么,有什么区别。nio的原理是什么
13.1. bio通信原理
服务端使用ServerSocket为每一个客户端建立一个线程用于通信。只要客户端还和服务端有连接,这个线程都要等待。
问题:超过几千客户端就不能够正常运行了
13.2. nio通信原理
每有一个客户端和服务端建立连接,都会创建一个channel,这些channel都会注册在selector中,这个selector只有一个线程。会不断轮询这些channel。,如果有请求过来,会创建一个线程来处理这个请求。处理完成后这个线程会被销毁。
可以对处理请求的线程创建一个线程池。
在工作线程和channel 直接可以维护一个cache
工作线程从channel 中读取数据,给channel写数据,是同步的
13.3. aio
对nio有优化:工作进程从channel读数据的时候,会绑定一个buffer,让操作系统来完成读操作,读完了来通知这个线程。
写的时候也是把写的过程交给操作系统。
13.4. 同步阻塞、同步非阻塞、异步非阻塞
BIO是同步阻塞,针对的是对磁盘文件的io读写。读写过程中线程阻塞
NIO是同步非阻塞,在操作系统读写数据的时候,线程可以做其他事情,但是也需要不断轮询判断读写完成了没有。
AIO 是异步非阻塞,发起文件读写的操作之后,交给操作系统,操作系统执行完毕之后,会通知这个线程。
13.5. BIO NIO AIO demo代码
TOREAD
14. 线上服务器问题
14.1. 线上CPU占用100%,排查步骤:
- top -c 输入P ,按照cpu进行排序
- top -Hp pid ,可以看到这个进程的负载
- 把线程pid换成16进制pidhex,如何使用jstack pid | grep pidhex -C5 –color 就可以定位到线程中哪行代码的cpu占用最高。
14.2. 如果线上进程kill不掉怎么办
ps aux 查看是否有僵尸进程 zombie
ps -ef | grep 僵尸进程id ,得到父进程id
然后kill 父进程之后kill子进程。
14.3. 磁盘马上占满了怎么办
是否是日志占满空间了?
经历:安装程序的时候提示根目录空间占用100%,发现是pcp(性能监控软件)的日志占用了很大空间。解决方法是直接使用rm -rf删除了这些日志/var/log/pcp/pmlogger/openEuler1/。
关于pcp:
- Performance Co-Pilot (
pcp
) 提供了支持系统级性能监控和管理的框架和服务。它为系统中的所有性能数据提供了统一的抽象,以及用于询问、检索和处理该数据的许多工具。 - 这些生成的log,在openeuler系统没有设置自动清理,导致了日志积累。
find / -size+100M | xargs ls -lh 找大于100m的文件
15. Java语言特性
15.1. 参数传递
- Java 中将实参传递给方法(或函数)的方式是 值传递:
- 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本
- 想要通过传值来修改原来的值
- 通过数组
- 通过类
- 或者其他可变的引用类型
- 不可变的引用类型
- String
- Integer
- BigDecimal
- LocalDate、LocalTime、LocalDateTime、Duration,Period
15.2. 序列化
序列化协议属于应用层或者传输层
序列化的对象:实现Serializable 接口的类、实例变量的值、非静态成员变量
serialVersionUID:用来判断对象版本。手动设置这个变量可以解决对象版本兼容问题。
Kryo用来序列化Java代码性能高。
import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import java.io.*; public class KryoSerializationExample { public static void main(String[] args) { // 创建 Kryo 对象 Kryo kryo = new Kryo(); // 创建要序列化的对象 Person person = new Person("Alice", 30); // 序列化 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Output output = new Output(outputStream); kryo.writeObject(output, person); output.close(); // 将序列化的数据保存到文件 try (FileOutputStream fileOutputStream = new FileOutputStream("person.dat")) { outputStream.writeTo(fileOutputStream); } catch (IOException e) { e.printStackTrace(); } // 反序列化 try (FileInputStream fileInputStream = new FileInputStream("person.dat")) { // 创建 Kryo 输入流 Input input = new Input(fileInputStream); // 从输入流中反序列化对象 Person deserializedPerson = kryo.readObject(input, Person.class); input.close(); // 使用反序列化后的对象 System.out.println("姓名: " + deserializedPerson.getName()); System.out.println("年龄: " + deserializedPerson.getAge()); } catch (IOException e) { e.printStackTrace(); } } }
15.3. 泛型和通配符
- 泛型(Generics):泛型允许在编译时指定类、接口或方法操作的数据类型,以提供类型安全和代码重用。通过使用泛型,可以在编译时捕获类型错误,并避免在运行时出现类型转换错误。
- 定义泛型类:使用
<T>
来表示类型参数,可以在类名后面声明一个泛型类型。例如:class MyClass<T> { ... }
。 - 定义泛型方法:使用
<T>
来表示类型参数,可以在方法返回类型前声明一个泛型类型。例如:<T> T myMethod(T obj) { ... }
。 - 约定
E
:表示集合中的元素类型。K
:表示映射中的键类型。V
:表示映射中的值类型。T
:表示任意类型。S
、U
、V
:用于表示第二、第三和第四类型参数。
- 定义泛型类:使用
- 类型通配符(Wildcard):类型通配符用问号
?
表示,用于灵活处理不同类型的泛型对象。通配符可以用于泛型类、泛型方法和通配符限定。- 通配符限定上界:
? extends Type
,表示泛型参数是 Type 类型或其子类。例如:List<? extends Number>
表示一个只能接受 Number 及其子类的 List。 - 通配符限定下界:
? super Type
,表示泛型参数是 Type 类型或其父类。例如:List<? super Integer>
表示一个只能接受 Integer 及其父类的 List。 - 无限制通配符:
?
,表示可以是任意类型。例如:List<?>
表示一个可以接受任意类型的 List。
- 通配符限定上界:
15.4. 反射
15.4.1.1. 基本操作
//获取类
Class<?> myClass = MyClass.class;
Class<?> myClass = Class.forName("com.example.MyClass");
//获取构造函数
Constructor<?> constructor = myClass.getDeclaredConstructor(parameterTypes);
//创建对象
Object myObject = constructor.newInstance(arguments);
//获取方法
Method method = myClass.getDeclaredMethod("methodName", parameterTypes);
//调用方法
method.invoke(myObject, arguments);
//获取字段
Field field = myClass.getDeclaredField("fieldName");
//获取字段的值
Object fieldValue = field.get(myObject);
//设置字段的值
field.set(myObject, value);
//对于私有方法或字段,可能需要使用setAccessible(true)来绕过访问限制。
15.5. 代理模式
15.5.1.1. 1.静态代理:
就是把在调用类的前后在执行一些步骤。
public class SmsProxy implements SmsService {
private final SmsService smsService;
public SmsProxy(SmsService smsService) {
this.smsService = smsService;
}
@Override
public String send(String message) {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method send()");
smsService.send(message);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method send()");
return null;
}
}
15.5.1.2. 2.动态代理
2.1JDK代理:在 Java 动态代理机制中 InvocationHandler
接口和 Proxy
类是核心。
//调用proxy的方法newProxyInstance
Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new DebugInvocationHandler(target));
//DebugInvocationHandler 是自定义的proxy方法,需要实现InvocationHandler接口的invoke方法,实际上是调用了这里的invoke方法
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DebugInvocationHandler implements InvocationHandler {
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
System.out.println("before Method"+ method.getName());
Object result = method.invoke(target, objects);
System.out.println("after Method"+ method.getName());
return result;
}
}
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
为了解决这个问题,我们可以用 CGLIB 动态代理机制。
2.2CGLIB代理
在 CGLIB 动态代理机制中
MethodInterceptor
接口和Enhancer
类是核心。maven依赖:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
//代理工厂中生成一个 enhancer对象,这个对象拥有下列属性,其中DebugMethodInterceptor拦截器是自定义的最终执行的方法 public class CglibProxyFactory { public static Object getProxy(Class<?> clazz){ Enhancer enhancer = new Enhancer(); enhancer.setClassLoader(clazz.getClassLoader()); enhancer.setSuperclass(clazz); enhancer.setCallback(new DebugMethodInterceptor()); return enhancer.create(); } } //DebugMethodInterceptor 实现MethodInterceptor接口,重写intercept方法,这个方法相当于前面的invoke public class DebugMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { //调用方法之前,我们可以添加自己的操作 System.out.println("before method " + method.getName()); Object object = methodProxy.invokeSuper(o, objects); //调用方法之后,我们同样可以添加自己的操作 System.out.println("after method " + method.getName()); return object; } }
2.3二者的区别
jdk动态代理,有接口的时候使用,生成一个实现这些同样接口的对象。
没有接口会使用cglib来生成你的类的子类,覆盖你的类的方法,在方法中加入增强的代码。
15.6. BigDecimal常见方法
我们在使用 BigDecimal
时,为了防止精度丢失,推荐使用它的BigDecimal(String val)
构造方法或者 BigDecimal.valueOf(double val)
静态方法来创建对象。
使用BigDecimal(double val)会丢失精度
方法:
- add:加
- subtract:减
- multiple:乘
- divide:除
- 除的时候尽量使用三个参数的版本:指定保留规则RoundingMode
- compareTo:
a.compareTo(b)
: 返回 -1 表示a
小于b
,0 表示a
等于b
, 1 表示a
大于b
。- 比较不能使用equals,因为equals比较会同时比较精度,1.0和1.00不相同
- setScale:保留小数
工具类:
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 简化BigDecimal计算的小工具类
*/
public class BigDecimalUtil {
/**
* 默认除法运算精度
*/
private static final int DEF_DIV_SCALE = 10;
private BigDecimalUtil() {
}
/**
* 提供精确的加法运算。
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.add(b2).doubleValue();
}
/**
* 提供精确的减法运算。
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double subtract(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的乘法运算。
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double multiply(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入。
*
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double divide(double v1, double v2) {
return divide(v1, v2, DEF_DIV_SCALE);
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入。
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double divide(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue();
}
/**
* 提供精确的小数位四舍五入处理。
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b = BigDecimal.valueOf(v);
BigDecimal one = new BigDecimal("1");
return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 提供精确的类型转换(Float)
*
* @param v 需要被转换的数字
* @return 返回转换结果
*/
public static float convertToFloat(double v) {
BigDecimal b = new BigDecimal(v);
return b.floatValue();
}
/**
* 提供精确的类型转换(Int)不进行四舍五入
*
* @param v 需要被转换的数字
* @return 返回转换结果
*/
public static int convertsToInt(double v) {
BigDecimal b = new BigDecimal(v);
return b.intValue();
}
/**
* 提供精确的类型转换(Long)
*
* @param v 需要被转换的数字
* @return 返回转换结果
*/
public static long convertsToLong(double v) {
BigDecimal b = new BigDecimal(v);
return b.longValue();
}
/**
* 返回两个数中大的一个值
*
* @param v1 需要被对比的第一个数
* @param v2 需要被对比的第二个数
* @return 返回两个数中大的一个值
*/
public static double returnMax(double v1, double v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.max(b2).doubleValue();
}
/**
* 返回两个数中小的一个值
*
* @param v1 需要被对比的第一个数
* @param v2 需要被对比的第二个数
* @return 返回两个数中小的一个值
*/
public static double returnMin(double v1, double v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.min(b2).doubleValue();
}
/**
* 精确对比两个数字
*
* @param v1 需要被对比的第一个数
* @param v2 需要被对比的第二个数
* @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1
*/
public static int compareTo(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.compareTo(b2);
}
}
15.7. Unsafe类
在JUC高并发编程中主要使用,用来执行本地方法(native方法)。
native方法的执行绕过了Java本身的界限。能直接接触到操作系统底层的某些功能,因此并不安全。
可以实现的功能:
- 内存操作
- 内存屏障
- 对象操作
- 数据操作
- CAS 操作
- 线程调度
- Class 操作
- 系统信息
因为其不安全性,并不推荐使用。
16. 算法
16.1. 字符串算法
16.1.1. 字符串最长匹配串(KMP)
核心:对于匹配串生成一个失配表,根据失配表进行匹配
失配表:匹配串的第几位没有匹配成功,再从匹配串的某一位重新匹配
16.1.2. 替换字符
调用replace方法即可
16.1.3. 最长前缀匹配
先利用 Arrays.sort(strs)为数组排序,再将数组第一个元素和最后一个元素的字符从前往后对比即可(sort的时候数字排在最前面)
16.1.4. 构建回文串
- 出现的字符次数是偶数的情况
- 出现的字符次数是偶数的组合+最长的奇数组合
16.1.5. 验证回文串
从两边向中间遍历
16.1.6. 最长回文子序列
**动态规划: **TOREAD
dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j) otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1])
16.1.7. 括号的匹配深度
while(replace(“()”,””) != -1)
16.1.8. 字符串转换为整数
实现 Integer.valueOf(string)的功能,但是 string 不符合数字要求时返回 0
//https://www.weiweiblog.cn/strtoint/
public class Main {
public static int StrToInt(String str) {
if (str.length() == 0)
return 0;
char[] chars = str.toCharArray();
// 判断是否存在符号位
int flag = 0;
if (chars[0] == '+')
flag = 1;
else if (chars[0] == '-')
flag = 2;
int start = flag > 0 ? 1 : 0;
int res = 0;// 保存结果
for (int i = start; i < chars.length; i++) {
if (Character.isDigit(chars[i])) {// 调用Character.isDigit(char)方法判断是否是数字,是返回True,否则False
int temp = chars[i] - '0';
res = res * 10 + temp;
} else {
return 0;
}
}
return flag != 2 ? res : -res;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
String s = "-12312312";
System.out.println("使用库函数转换:" + Integer.valueOf(s));
int res = Main.StrToInt(s);
System.out.println("使用自己写的方法转换:" + res);
}
}
16.2. 链表算法
16.2.1. 两数相加
16.2.2. 反转链表
16.2.3. 链表中倒数第K个数
两个节点同时向后,第一个节点走到第k个,第二个再开始走。
第一个节点到末尾,第二个节点就到倒数第k个
16.2.4. 合并两个排序的链表
16.3. 部分常见题目
16.3.1. 斐波那契数列
递归法:容易因为重复计算超时
迭代法:
int Fibonacci(int number) {
if (number <= 0) {
return 0;
}
if (number == 1 || number == 2) {
return 1;
}
int first = 1, second = 1, third = 0;
for (int i = 3; i <= number; i++) {
third = first + second;
first = second;
second = third;
}
return third;
}
16.3.2. 跳台阶:也是斐波那契数列
16.3.3. 变态跳台阶:一个可以跳n个
假设 n>=2,第一步有 n 种跳法:跳 1 级、跳 2 级、到跳 n 级 跳 1 级,剩下 n-1 级,则剩下跳法是 f(n-1) 跳 2 级,剩下 n-2 级,则剩下跳法是 f(n-2) …… 跳 n-1 级,剩下 1 级,则剩下跳法是 f(1) 跳 n 级,剩下 0 级,则剩下跳法是 f(0) 所以在 n>=2 的情况下: f(n)=f(n-1)+f(n-2)+…+f(1) 因为 f(n-1)=f(n-2)+f(n-3)+…+f(1) 所以 f(n)=2f(n-1) 又 f(1)=1,所以可得*f(n)=2^(number-1)
java 中有三种移位运算符:
- “<<” : 左移运算符,等同于乘 2 的 n 次方
- “>>”: 右移运算符,等同于除 2 的 n 次方
- “>>>” : 无符号右移运算符,不管移动前最高位是 0 还是 1,右移后左侧产生的空位部分都以 0 来填充。与>>类似。
16.3.4. 二维数组查找
16.3.5. 调整数组顺序使奇数位于偶数前面
思路:统计奇数个数,假设为n
遍历数组,奇数从0存,偶数从n存
16.3.6. 两个栈实现队列
16.3.7. 栈的压入弹出序列
判断弹出序列是否合法。使用两个栈,第一个栈出的元素不是序列要的,就压入弹出序列,如果原栈为空,弹出栈的栈首不是要求序列则不对。
16.4. 排序算法
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 2738430398@qq.com