阅读 131

Java知识积累-基础篇

把前段时间看过的内容,结合面试经历,作以整理,持续改进:D

Java基础

JVM

emmmm,为什么把JVM放第一个咧……

主要是因为之前某次面试的时候被问到“从宏观角度怎么看Java”才发现用了N年的Java竟然都没好好看过这个

回想一下也是这感觉,从大二学Java以来一直没把JVM当回事,回过来看才发现错过了一个亿orz

Java跨平台的特性,主要归功于Java虚拟机JVM——Java virtual machine

JVM内存结构

如图,通过编译.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) 算法

标记可回收的对象,然后进行清除。

如图,清理了红色标记的部分,清理结束后多出绿色部分可用。

存在问题:

  1. 标记-清除过程的效率有限
  2. 内存碎片化

2. 复制(Copying) 算法

预留同样大小的一块内存,进行GC时复制存活的对象到复制用区并且顺序放置(不留空隙)。

优势:避免碎片化问题;

问题:复制需保留一块内存,导致内存浪费

3. 标记-整理(Mark-Compact) 算法

类似于标记-清除,但是需对存活者进行整理

优势:避免碎片化问题

*.分代收集算法(Generational Collection)

分代收集算是对前面3种方案的综合

大致分为3块区域:

  1. Eden: 职业萌新,日常冒泡但是存活时间极短,经常被回收
  2. Survivor(From+To):活下来的萌新能够感受到大佬的注视,并且随着灌水时间增加,会从[From区]移动到[To区]
  3. Old:群内老油条(存活时间长) 亦或是[Eden区]和[Survivor区]放不下的大号对象(进来就是巨佬)

萌新大佬分布图:

GC执行分布图:

每次看到这个的甚至怀疑Java内存回收机制的设计者和JOJO作者有一腿,Dio的时间停止和Stop-the-world竟然如此的切合

咳咳,如图所示[Eden区]和[Survivor区]的对象一般被普通MinorGC回收,回收频率高;但是[Old区]中的老油条岂是随随便便就能被回收的?那么是时候请出我们的MajorGC Dio:砸瓦鲁多 触发Stop-the-world进行垃圾回收。

内存模型(JMM)

看到 Hollis大佬 的一文——《求你了,再问你Java内存模型的时候别再给我讲堆栈方法区了…》 猛然惊醒竟然忘了记这块内容了orz

补血ing

基础类型

避免遗漏,通过类型区分,由小到大排序:

  • 整形
    • 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.

官方文档《Oracle-Java教程-原始数据类型》

包装类型

  • 基础类型都有对应的包装类型(Integer、Char等

  • 在基础类型和包装类型之间通过自动装箱拆箱完成赋值

    • Integer i = 3; 装箱
    • int j = i; 拆箱
  • 默认值存在差异

    • 基础类型默认值大都取0
    • 包装类默认大都取null
  • 类型比较( ==和equals() )需注意

    • Integer var = ? 在[-128,127]范围内的赋值,Integer 对象通过IntegerCache.cache产生,会复用已有对象,这个区间内的Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,推荐使用equals()进行比较
    基本类型 == equals
    字符串变量 对象在内存中的首地址 字符串内容
    非字符串变量 对象在内存中的首地址 对象在内存中的首地址
    基本类型 不可用
    包装类 地址 内容

    表格参考自——alexyyek.github.io/2014/12/29/…

字符串

  • String, StringBuffer, StringBuilder, StringJoiner

    1. String 内容不可变(赋值改变的是引用)

      String s = "123"; //s只保存“123”的引用地址
      s = "456"; // s保存了“456”的引用地址,"123"还鸽在内存里等回收
      复制代码
    2. StringBuffer 线程安全Synchronized

    3. StringBuilder 非线程安全

    4. 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);
    }
    复制代码
    1. 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......
    复制代码
    1. 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.....
    复制代码
    1. 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......
    复制代码
    1. 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......]
    复制代码
    1. streamCollection.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接口

多态

参考:www.runoob.com/java/java-p…

  1. 重写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){...}
} 
复制代码
  1. 实现抽象类的抽象方法 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(); // 执行实现类实现的方法
        }
    }
}
// 继承→实现→向上转型
复制代码
  1. 接口

这种实现方式在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.forNamejava.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为了看反射竟然把类加载机制都看了一圈

类加载的过程

参考 网易云课堂-微专业-Java高级

  1. 加载(Loading)

    读取二进制内容

  2. 验证(Verification)

    验证class文件格式规范、语义分析、引用验证、字节码验证

  3. 准备(Preparation)

    分配内存、设置类static修饰的变量初始值(此时除了final修饰以外的其他static变量均先给0或null值,只有final值是在此时确定的)

  4. 解析(Resolution)

    类、接口、字段、类方法等解析

  5. 初始化(Initialization)

    静态变量赋值(对,static的值现在才给赋上),执行静态代码块

  6. 使用(Using)

    创建实例对象

  7. 卸载(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…

  1. 创建阶段(Created)

  2. 应用阶段(In Use)

    对象至少被一个强引用持有

  3. 不可见阶段(Invisible)

    超出作用域

    for(int i = 0; i < 10; i++){ // in loop
        // i is visible
    }// out of the loop
    // i is invisible
    复制代码
  4. 不可达阶段(Unreachable)

    无任何强引用持有

  5. 收集阶段(Collected)

    gc已经对该对象的内存空间重新分配做好准备

  6. 终结阶段(Finalized)

    当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收

  7. 对象空间重分配阶段(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

github.com/CyC2018/CS-…

和流操作基本相似,也是在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

参考 网易云课堂-微专业-Java高级

三大核心组件:

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(); // 清除整个缓冲区
复制代码

三属性

  1. capacity 容量
  2. position 位置
  3. limit 限制

俩模式

  1. 写入模式
  2. 读取模式

以上方的使用为例解析: 容量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

基本操作:

  1. 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();
复制代码
  1. 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常量来存储:

  1. 连接: SelectionKey.OP_CONNECT
  2. 准备就绪: SelectionKey.OP_ACCEPT
  3. 读取: SelectionKey.OP_READ
  4. 写入: 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();
    }
}
复制代码
关注下面的标签,发现更多相似文章
评论