把前段时间看过的内容,结合面试经历,作以整理,持续改进:D
Java基础
JVM
emmmm,为什么把JVM放第一个咧……
主要是因为之前某次面试的时候被问到“从宏观角度怎么看Java”才发现用了N年的Java竟然都没好好看过这个
回想一下也是这感觉,从大二学Java以来一直没把JVM当回事,回过来看才发现错过了一个亿orz
Java跨平台的特性,主要归功于Java虚拟机JVM——Java virtual machine
如图,通过编译.java源码文件获得.class字节码文件,然后JVM通过类加载器加载字节码文件,在虚拟机中运行
运行时的数据区主要有两块:线程共享区和独占区/非共享区
例如一块psvm里面Idea党运行的代码:
String s = new String("123");
复制代码
将它拆分为数个部分并对照上图可得:
String s = xxxx
→ 对象s是保存字符串的引用的本地变量 → 本地方法栈Stack(独占区)new String(xxxx)
→ 通过带参数的构造器实例化String对象并返回其引用 → 堆内存Heap(共享区)"123"
→ 字符串常量 → 常量池 → 方法区Non-Heap(共享区)
这样一解析,运行时数据存储的区域就清晰很多了:D
Garbage Collection 垃圾回收
刚好前面恒生群面的时候有同学被问到了,这里也趁这机会总结一下
3种垃圾回收算法
1. 标记-清除(Mark-Sweep) 算法
标记可回收的对象,然后进行清除。
存在问题:
- 标记-清除过程的效率有限
- 内存碎片化
2. 复制(Copying) 算法
预留同样大小的一块内存,进行GC时复制存活的对象到复制用区并且顺序放置(不留空隙)。
优势:避免碎片化问题;
问题:复制需保留一块内存,导致内存浪费
3. 标记-整理(Mark-Compact) 算法
类似于标记-清除,但是需对存活者进行整理
基础类型
避免遗漏,通过类型区分,由小到大排序:
- 整形
- byte 字节 8-bit, -128~127
- short 短整型 16-bit, -32,768~32,767
- int 整形 32-bit
- long 长整型 64-bit
- 浮点
- float 单精度浮点 32-bit
- double 双精度浮点 64-bit
- 特殊
- char 单字符 a single 16-bit Unicode character, 0~65,535
- boolean 布尔值
represents 1 bit of information, but its "size" isn't something that's precisely defined.
包装类型
-
基础类型都有对应的包装类型(Integer、Char等
-
在基础类型和包装类型之间通过自动装箱拆箱完成赋值
Integer i = 3;
装箱int j = i;
拆箱
-
默认值存在差异
- 基础类型默认值大都取0
- 包装类默认大都取null
-
类型比较( ==和equals() )需注意
- Integer var = ? 在[-128,127]范围内的赋值,Integer 对象通过IntegerCache.cache产生,会复用已有对象,这个区间内的Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,推荐使用equals()进行比较
基本类型 == equals 字符串变量 对象在内存中的首地址 字符串内容 非字符串变量 对象在内存中的首地址 对象在内存中的首地址 基本类型 值 不可用 包装类 地址 内容
字符串
-
String, StringBuffer, StringBuilder, StringJoiner
-
String 内容不可变(赋值改变的是引用)
String s = "123"; //s只保存“123”的引用地址 s = "456"; // s保存了“456”的引用地址,"123"还鸽在内存里等回收 复制代码
-
StringBuffer 线程安全Synchronized
-
StringBuilder 非线程安全
-
StringJoiner Java 1.8新宝贝,非线程安全(实际通过StringBuilder实现), [文档]
- 线程安全性:String > StringBuffer > StringBuilder ≈ StringJoiner
- 内存消耗:String > StringBuffer > StringBuilder ≈ StringJoiner
- 执行效率: String < StringBuffer < StringBuilder ≈ StringJoiner
-
-
字符串拼接问题
众所周知String在循环里做拼接会耗时间耗内存,就想看看耗到什么程度
代码paste,若有问题欢迎指正qwq
- 准备一个String的List,其他方法写成静态方法调用并返回String结果
List<String> stringList = new ArrayList<>(); for (int i = 0; i < N; i++) { stringList.add(""+i); } for (int i = 1; i <= 5; i++) { runTestWithClock(i, stringList); // Thread.sleep(10000L); //用于查看内存时分隔每种方法 } 复制代码
- 准备一个计时器
private static void runTestWithClock(int n, List<String> stringList){ String result = null; long clock = System.currentTimeMillis(); // 记录运行前时间 long timer = 0; System.out.println("--------"+n+"-------"); switch (n){ case 1: result = sTest(stringList); break; case 2: result = sBufferTest(stringList); break; case 3: result = sBuilderTest(stringList); break; case 4: result = sJoiner(stringList); break; case 5: result = sjStreamTest(stringList); } timer = System.currentTimeMillis() - clock ; // 计算时间差 System.out.println("Timer: "+timer); System.out.println(result); } 复制代码
- String +
String result = ""; for (String s: stringList){ result = result + s + ","; } return result; // sTest:0,1,2,3,4,5,6,7,8,9,10,11,12...... 复制代码
- StringBuffer append()
StringBuffer sBuffer = new StringBuffer(); for (String s: stringList){ sBuffer.append(s).append(","); } return sBuffer.toString(); // sBufferTest:0,1,2,3,4,5,6,7,8,9,10,11..... 复制代码
- StringBuilder append()
StringBuilder sBuilder = new StringBuilder(); for (String s: stringList){ sBuilder.append(s).append(","); } return sBuilder.toString(); // sBuilderTest:0,1,2,3,4,5,6,7,8,9,10,11...... 复制代码
- StringJoiner add()
/* StringJoiner Since Java1.8 * @param:分隔符,前缀,后缀 * Docs: https://docs.oracle.com/javase/8/docs/api/java/util/StringJoiner.html */ StringJoiner sj = new StringJoiner(",", "[", "]"); for (String s: stringList){ sj.add(s); } return sj.toString(); // sJoiner:[0,1,2,3,4,5,6,7,8,9,10,11......] 复制代码
- stream 和 Collection.joining (stream特性详见Java进阶-语法糖-Streams API)
return stringList.stream(). map(String::toString). collect(Collectors.joining(",", "[", "]")); // sjStreamTest:[0,1,2,3,4,5,6,7,8,9,10,11......] 复制代码
- 当N为100000时, 执行效率显然String最惨
--------1-------String Timer: 26841 --------2-------StringBuffer Timer: 14 --------3-------StringBuilder Timer: 11 --------4-------StringJoiner Timer: 10 --------5-------stream+joining Timer: 43 复制代码
- 正常状态下的内存消耗曲线(Java VisualVM-监视-内存-堆)
- 执行代码时,出现了明显增幅
- 查看时间,均在使用String相加时发生,sleep10秒后执行其他方法没有出现增幅明显的迹象
面向对象编程
封装
public class User {
private String userName;
private String password;
public User() {}
public User(String userName, String password) {
this.userName = userName;
this.password = password;
}
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
复制代码
对象的属性和方法封装起来,不可直接暴露给外部访问(例如对象实例user的password属性不能用user.password直接获取或修改),只能通过实例化对象的方法访问内部属性(user.getPassword())
继承
public class VIP extends User{
private int vipId;
private int accessType;
public VIP() {}
public VIP(int vipId, int accessType) {
// super(); // 隐藏
this.vipId = vipId;
this.accessType = accessType;
}
public VIP(String userName, String password, int vipId, int accessType) {
super(userName, password);
this.vipId = vipId;
this.accessType = accessType;
}
// 省略getter, setter
}
复制代码
VIP继承自User,包含User所有属性和方法,在此基础上又拥有自己独立的属性和方法
- 向上转型
User user = new VIP();
有个疑问,使用List list = new ArrayList<>();
算不算向上转型,一方面ArrayList继承自抽象类AbstractList,另一方面实现了List接口
多态
- 重写overwrite
class User {
public void communicate(){ System.out.println("User is communicating."); }
}
class Teacher extends User{ //继承自User
public void communicate(){ System.out.println("Teacher is communicating."); } //重写超类的方法
}
class Student extends User{ //继承自User
public void communicate(){ System.out.println("Student is communicating."); } //重写超类的方法
}
public class Communication {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
users.add(new Teacher()); //向上转型
users.add(new Student()); //向上转型
for( User user: users ){
user.communicate(); // 执行子类重写的方法
}
}
}
// 继承→重写→向上转型
复制代码
- 区别→重载overload
- 重写需要方法名、参数、返回值类型都相同
- 重载需要方法名相同但是参数不同,返回值类型可以相同也可以不同
public class Student{
public Course searchCourse(int courseId){...}
public Course searchCourse(String courseName){...}
public List<Course> searchCourse(String instructorName, String courseType){...}
}
复制代码
- 实现抽象类的抽象方法 User可改为抽象类,communicate()改成抽象方法
abstract class User {
public abstract void communicate();
}
class Teacher extends User{ //继承自User
public void communicate(){ System.out.println("Teacher is communicating."); } //实现超类的抽象方法
}
class Student extends User{ //继承自User
public void communicate(){ System.out.println("Student is communicating."); } //实现超类的抽象方法
}
public class Communication {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
users.add(new Teacher()); //向上转型
users.add(new Student()); //向上转型
for( User user: users ){
user.communicate(); // 执行实现类实现的方法
}
}
}
// 继承→实现→向上转型
复制代码
- 接口
这种实现方式在MVC里挺常用, 业务层Service和数据处理层Dao(或Repository)都会应用, 此处以Service为例,不同的实现使用的是不同的数据处理层
public interface UserService { User findUserById(Integer userId); }
public class UserServiceImplByRepository implements UserService {
private UserRepository userRepository;
@Override
public User findUserById(Integer userId) { return userRepository.findUserByUserId(userId); }
}
public class UserServiceImplByDao implements UserService {
private UserDao userDao;
@Override
public User findUserById(Integer userId) { return userDao.findUserByUserId(userId); }
}
public class UserController {
public String user(int id){
if( id < 1000 ){
// 假设1000以下的使用Repository的实现
UserService us = new UserServiceImplByRepository();
// UserService接口类型的变量引用指向UserServiceImpl实现类的对象
return us.findUserById(id);
// 此处使用的是UserServiceImplByRepository的方法
}else{
// 其他使用Dao的实现
UserService us = new UserServiceImplByDao();
// UserService接口类型的变量引用指向UserServiceImplByDao实现类的对象
return us.findUserById(id) ;
// 此处使用的是UserServiceImplByDao的方法
}
}
}
复制代码
反射
一开始用JDBC的时候还不知道,回过头来才发现早已在用了……
写完Class.forName("com.mysql.jdbc.Driver")
就能用Connection conn = DriverManager.getConnection(DB_URL,USER,PASS);
来拿Connection连接
可以看看JDBC例子回顾一下:D www.tutorialspoint.com/jdbc/jdbc-s…
实际上手看看Class.forName
和java.lang.reflect
怎么用:
public class ReflectTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> userClass = Class.forName("cn.magiklau.java.basic.model.User");
// 输出Class名
System.out.println("Class name: "+userClass.getName());
Collector<CharSequence, ?, String> joining = Collectors.joining(", ", "[", "]"); // 方便格式化输出
System.out.print("DeclaredFields: ");
// 使用getFields()只能取得public属性,这里要取private属性需要用getDeclaredFields()
System.out.println(Arrays.stream(userClass.getDeclaredFields()).map(Field::getName).collect(joining));;
System.out.print("DeclaredMethods: ");
// 使用getMethods()也会取得超类的方法,这里还是只取本身的
System.out.println(Arrays.stream(userClass.getDeclaredMethods()).map(Method::getName).collect(joining));
// 取一个实例
User user = (User)userClass.newInstance();
user.setUserName("testUserName");
System.out.println("Username: "+user.getUserName());
}
}
运行结果:
Class name: cn.magiklau.java.basic.model.User
DeclaredFields: [userName, password]
DeclaredMethods: [getPassword, setUserName, getUserName, setPassword]
Username: testUserName
复制代码
参考文章
深入理解 Java 反射和动态代理 github.com/dunwu/blog/…
深入解析Java反射(1) - 基础 www.sczyh30.com/posts/Java/…
类加载机制
orz为了看反射竟然把类加载机制都看了一圈
类加载的过程
-
加载(Loading)
读取二进制内容
-
验证(Verification)
验证class文件格式规范、语义分析、引用验证、字节码验证
-
准备(Preparation)
分配内存、设置类static修饰的变量初始值(此时除了final修饰以外的其他static变量均先给0或null值,只有final值是在此时确定的)
-
解析(Resolution)
类、接口、字段、类方法等解析
-
初始化(Initialization)
静态变量赋值(对,static的值现在才给赋上),执行静态代码块
-
使用(Using)
创建实例对象
-
卸载(Unloading)
从JVM方法区中卸载(条件:① 该Class所有实例都已经被GC;② 加载该类的ClassLoader实例已经被GC)
双亲委派模型
除了顶层ClassLoader以外,其他类加载器都需要有超类加载器,在超类加载失败时才会交给子类加载
// 竟然还不支持MarkDown画图……简单记一下先
Bootstrap ClassLoader
顶层启动类加载器
↑委托 ↓查找
Extension ClassLoader
拓展类库类加载器
↑委托 ↓查找
Application ClassLoader
用户应用程序类加载器
↑委托 ↓查找
Custome ClassLoader
自定义类加载器
复制代码
又看到一些个神奇的做法,备用 www.cnblogs.com/chanshuyi/p…
对象生命周期
参考CSDN-Sodino-blog.csdn.net/sodino/arti…
-
创建阶段(Created)
-
应用阶段(In Use)
对象至少被一个强引用持有
-
不可见阶段(Invisible)
超出作用域
for(int i = 0; i < 10; i++){ // in loop // i is visible }// out of the loop // i is invisible 复制代码
-
不可达阶段(Unreachable)
无任何强引用持有
-
收集阶段(Collected)
gc已经对该对象的内存空间重新分配做好准备
-
终结阶段(Finalized)
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收
-
对象空间重分配阶段(De-allocated)
IO
图来自:runoob - Java 流(Stream)、文件(File)和IO - www.runoob.com/java/java-f…
字节流 InputStream OutputStream
// 读操作
String filePath = "C:\\WorkSpace\\JavaProject\\JavaLearning\\src\\ioTestFile.txt";
// 字节流输入,FileInputStream提供文件处理功能
InputStream fis = new FileInputStream(filePath);
// BufferedInputStream提供缓存功能
InputStream bis = new BufferedInputStream(fis);
while( bis.available() > 0 ){
System.out.println((char) bis.read());
}
// 同理进行写操作
OutputStream fos = new FileOutputStream(filePath);
OutputStream bos = new BufferedOutputStream(fos);
bos.write("MagikIOTest~".getBytes());
bos.flush();
复制代码
字符流 Reader Writer
和流操作基本相似,也是在Reader里叠上File和Buffered装饰
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String gotLine;
while ((gotLine = bufferedReader.readLine()) != null) {
System.out.println(gotLine);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
FileWriter fileWriter = new FileWriter(filePath);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write("MagikIOTest writer~");
bufferedWriter.close();
复制代码
网络 Socket
Server: ServerSocket(port, timeout)
Client: Socket(host, port)
Server: accept()
S/C: InputSteam->OutputSteam
S/C: close()
NIO
非阻塞Non-Block IO
三大核心组件:
1. Buffer 缓冲区
2. Channel 通道
3. Selector 选择器
复制代码
1. Buffer 缓冲区
使用:
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(n); // 申请容量n
byteBuffer.put((byte) 1); // 写
byteBuffer.flip(); // 转换读写
byte b = byteBuffer.get(); // 读
byteBuffer.compact(); // 清除已阅读的数据。转为写入模式
byteBuffer.clear(); // 清除整个缓冲区
复制代码
三属性
- capacity 容量
- position 位置
- limit 限制
俩模式
- 写入模式
- 读取模式
以上方的使用为例解析: 容量c,位置p,限制l
[Write mode] 初始
0 1 2 3
↑ ↑↑
p lc
===
put(x)
===
[Write mode] 完成put
0 1 2 3
↑ ↑↑
p lc
===
flip()
===
[Read mode] 转换模式
0 1 2 3
↑ ↑ ↑
p l c
===
get()
===
[Read mode] 完成get
0 1 2 3
↑↑ ↑
pl c
===
compact()
===
[Read mode] 清除已读
0 1 2 3
x ↑↑ ↑
x pl c
[Write mode] 并转换模式
0 1 2 3
↑ ↑↑
p lc
===
clear()
===
返回初始状态
0 1 2 3
↑ ↑↑
p lc
复制代码
2. Channel 通道
运作方式:
- BIO-Send: data -> byte[] -> outputStream
- BIO-Receive: inputStream -> read -> data
- NIO-Send: data -> buffer -> channel
- NIO-Receive: channel -> buffer -> data
API:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
基本操作:
- Client- SocketChannel
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false); // 设置为非阻塞模式
sc.connect( new InetSocketAddress("http://domain.com", port));
sc.write(byteBuffer);
sc.read(byteBuffer);
sc.close();
复制代码
- Server- ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 同理非阻塞
ssc.socket().bind(new InetSocketAddress(8080));
while(true){
SocketChannel sc = ssc.accept();
if( sc != null ){
sc.write(xxx)
sc.read(xxx)
....
}
}
复制代码
Really? While loop? Need to be improved:
3. Selector 选择器
用于管理多个NIO通道。
channel事件类型使用SelectionKey常量来存储:
- 连接: SelectionKey.OP_CONNECT
- 准备就绪: SelectionKey.OP_ACCEPT
- 读取: SelectionKey.OP_READ
- 写入: SelectionKey.OP_WRITE
使用:
// 创建选择器
Selector selector = Selector.open();
selector.configureBlocking(false);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 注册通道
SelectionKey key = ssc.register(selector, SelectionKey.OP_READ);
while(true){ // 一直监听,安全保障
int readyChannels = selector.select(); // 监听事件,阻塞到有为止
if( readyChannels == 0 ) continue; // 返回监听状态
// 成功监听到事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 判断事件类型
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
}
复制代码