Java基础

Posted by DEVIN on Sun, Jun 18, 2023

数据类型

INFINITY和NaN

 1// INFINITY定义
 2public static final double POSITIVE_INFINITY = 1.0 / 0.0;
 3public static final double NEGATIVE_INFINITY = -1.0 / 0.0;
 4
 5public static final float POSITIVE_INFINITY = 1.0f / 0.0f;
 6public static final float NEGATIVE_INFINITY = -1.0f / 0.0f;
 7
 8// 无穷大*0=NAN
 9System.out.println(Float.POSITIVE_INFINITY * 0); // output: NAN
10
11// 无穷大
12System.out.println((Float.POSITIVE_INFINITY / 0) == Float.POSITIVE_INFINITY); // output: true
13System.out.println(Float.POSITIVE_INFINITY == (Float.POSITIVE_INFINITY + 10000)); // output: true
14System.out.println(Float.POSITIVE_INFINITY == (Float.POSITIVE_INFINITY / 10000)); // output: true
15
16// 判断是否为INFINITY
17System.out.println(Double.isInfinite(Float.POSITIVE_INFINITY)); // output: true
1// NAN定义
2public static final double NaN = 0.0d / 0.0;
3
4// NAN表示非数字,它与任何值都不相等,甚至不等于它自己,所以要判断一个数是否为NAN要用isNAN方法:
5System.out.println(Float.NaN == Float.NaN); // output: false
6System.out.println(Double.isNaN(Float.NaN)); // output: true

需要精确计算时,使用BigDecimal,不要使用float和double

判断相等

float不能使用==判断相等,Float不能使用equals()flt.compareTo(another) == 0比较大小。可以用使用Math.abs(f1 - f2) < 1e-6f

Float f1float f2使用==比较大小时,会先把f1拆箱,然后相当于float比较。

IO操作

IO流是一种流式的数据输入/输出模型:

  • 二进制数据以byte为最小单位在InputStream/OutputStream中单向流动;
  • 字符数据以char为最小单位在Reader/Writer中单向流动。

Java标准库的java.io包提供了同步IO功能:

  • 字节流接口:InputStream/OutputStream
  • 字符流接口:Reader/Writer

File类

Java标准库的java.io.File对象表示一个文件或者目录:

  • 创建File对象本身不涉及IO操作;
  • 可以获取路径/绝对路径/规范路径:getPath()/getAbsolutePath()/getCanonicalPath()
  • 可以获取目录的文件和子目录:list()/listFiles()
  • 可以创建或删除文件和目录。

InputStream/OutputStream

Java标准库的java.io.InputStream定义了所有输入流的超类:

  • FileInputStream实现了文件流输入,==需要关闭流==;
  • ByteArrayInputStream在内存中模拟一个字节流输入,==不需要关闭流==。

在解压图片的时候发现ByteArrayOutputStream不需要关闭,为啥呢?

ByteArrayOutputStreamByteArrayInputStream是内存读写流,不同于指向硬盘的流,它内部是使用字节数组读内存的,这个字节数组是它的成员变量,当这个数组不再使用变成垃圾的时候,Java的垃圾回收机制会将它回收。所以不需要关流。

文件流 FileInputStream

  1. try…finally

切记释放资源,即input.close();

 1public void readFile() throws IOException {
 2    InputStream input = null;
 3    try {
 4        input = new FileInputStream("src/readme.txt");
 5        int n;
 6        while ((n = input.read()) != -1) { // 利用while同时读取并判断
 7            System.out.println(n);
 8        }
 9    } finally {
10        if (input != null) { input.close(); } // 释放资源
11    }
12}
13
14
15public void writeFile() throws IOException {
16    OutputStream output = new FileOutputStream("out/readme.txt");
17    output.write("Hello".getBytes("UTF-8")); // Hello
18    output.close();
19}
  1. try(resource) Java7引入

实际上,编译器并不会特别地为InputStream加上自动关闭。编译器只看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。InputStreamOutputStream都实现了这个接口,因此,都可以用在try(resource)中。

1public void readFile() throws IOException {
2    try (InputStream input = new FileInputStream("src/readme.txt")) {
3        int n;
4        while ((n = input.read()) != -1) {
5            System.out.println(n);
6        }
7    } // 编译器在此自动为我们写入finally并调用close()
8}
  1. 阻塞

在调用InputStreamread()方法读取数据时,我们说read()方法是阻塞(Blocking)的。它的意思是,对于下面的代码: 和InputStream一样,OutputStreamwrite()方法也是阻塞的。

1int n;
2n = input.read(); // 必须等待read()方法返回才能执行下一行代码
3int m = n;

字节流 ByteArrayInputStream

1public void readBuffer throws IOException {
2    byte[] data = { 72, 101, 108, 108, 111, 33 };
3    try (InputStream input = new ByteArrayInputStream(data)) {
4        int n;
5        while ((n = input.read()) != -1) {
6            System.out.println((char)n);
7        }
8    }
9}
1// 读取input.txt,写入output.txt:
2try (InputStream input = new FileInputStream("input.txt");
3     OutputStream output = new FileOutputStream("output.txt"))
4{
5    input.transferTo(output); // transferTo的作用是?
6}

序列化 ObjectOuputStream

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。

一个类的对象要想序列化成功,必须满足两个条件:

  1. 该类必须实现java.io.Serializable接口。Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
  2. 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂(transient)的。
  3. static修饰的属性将不被序列化。
 1public class Employee implements java.io.Serializable
 2{
 3    public String name;
 4    public String address;
 5    public transient int SSN;
 6    public int number;
 7    public void mailCheck() {
 8        System.out.println("Mailing a check to " + name + " " + address);
 9    }
10}

把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流:

 1public void test(String[] args) throws IOException {
 2    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
 3    // FileOutputStream stream = new FileOutputStream(new File("person.out")); // 也可以用文件流,但是记得关闭流
 4    try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
 5        // 写入int:
 6        output.writeInt(12345);
 7        // 写入String:
 8        output.writeUTF("Hello");
 9        // 写入实现了Serializable接口的Object:
10        output.writeObject(Double.valueOf(123.456));
11    }
12
13    System.out.println(Arrays.toString(buffer.toByteArray()));
14}

反序列化 ObjectInputStream

除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。

readObject()可能抛出的异常有:

  • ClassNotFoundException:没有找到对应的Class;
  • InvalidClassException:Class不匹配。
1try (ObjectInputStream input = new ObjectInputStream(...)) {
2    int n = input.readInt();
3    String s = input.readUTF();
4    Double d = (Double) input.readObject();
5}

对于ClassNotFoundException,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。

对于InvalidClassException,为了避免class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,不兼容时就会抛出此异常。

1public class Person implements Serializable {
2    private static final long serialVersionUID = 2709425275741743919L;
3}

垃圾回收

finalize用法

finalize()是Object类里的protected类型的方法,子类(所有类都是Object的子类)可以通过覆盖这个方法来实现回收前的资源清理工作。

由于重写finalize不当,会导致该对象无法回收,所以在项目里,我们一般不重写该方法,而会采用Object类自带的空的finalize方法。

  1. Java虚拟机一旦通过刚才提到的“根搜索算法”判断出某对象处于可回收状态时,会判断该对象是否重写了Object类的finalize方法,如果没,则直接回收。
  2. 如重写过finalize方法,而且未执行过该方法,则把该对象其放入F-Queue队列,另个线程会定时遍历F-Queue队列,并执行该队列中各对象的finalize方法。
  3. finalize方法执行完毕后,GC会再次判断该对象是否可被回收,如果可以,则进行回收,如果此时该对象上有强引用,则该对象“复活”,即处于“不可回收状态”。
 1public class FinalizeDemo {
 2    static FinalizeDemo obj = null;
 3    //重写Object里的finalize方法
 4    protected void finalize() throws Throwable {
 5       System.out.println("In finalize()");
 6       obj = this; //给obj加个强引用
 7    }
 8    public static void main(String[] args) throws InterruptedException {
 9        obj = new FinalizeDemo();
10        obj = null; //去掉强引用
11        System.gc(); //垃圾回收
12        // sleep 1秒,以便垃圾回收线程清理obj对象
13        Thread.sleep(1000);
14        if (null != obj) { // 在finalize方法复活
15            System.out.println("Still alive.");
16        } else {
17            System.out.println("Not alive.");
18        } 
19    }  
20}

强引用、软引用、弱引用、虚引用

理解Java的强引用、软引用、弱引用和虚引用 - 掘金 (juejin.cn)

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 当内存不足时 对象缓存 内存不足时终止
弱引用 正常垃圾回收时 对象缓存 垃圾回收后终止
虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止。如下:

1	Object strongReference = new Object();
2
3	// 如果强引用对象不使用时,需要弱化从而使GC能够回收
4	strongReference = null;

显式地设置strongReference对象为null,或让其超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于GC算法。

在一个方法的内部有一个强引用,这个引用保存在Java中,而真正的引用内容(Object)保存在Java中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数0,这个对象会被回收。但是如果这个strongReference全局变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。

软引用

如果一个对象只具有软引用,则内存空间充足时,垃圾回收器不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

1    // 强引用
2    String strongReference = new String("abc");
3    // 软引用
4    String str = new String("abc");
5    SoftReference<String> softReference = new SoftReference<String>(str);

软引用可用来实现内存敏感的高速缓存。

注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。

应用场景:

浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

  1. 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
  2. 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
 1    // 获取浏览器对象进行浏览
 2    Browser browser = new Browser();
 3    // 从后台程序加载浏览页面
 4    BrowserPage page = browser.getPage();
 5    // 将浏览完毕的页面置为软引用
 6    SoftReference softReference = new SoftReference(page);
 7
 8    // 回退或者再次浏览此页面时
 9    if (softReference.get() != null) {
10        // 内存充足,还没有被回收器回收,直接获取缓存
11        page = softReference.get();
12    } else {
13        // 内存不足,软引用的对象已经回收
14        page = browser.getPage();
15        // 重新构建软引用
16        softReference = new SoftReference(page);
17    }

弱引用

弱引用软引用的区别在于:只具有弱引用的对象拥有更短暂生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现那些只具有弱引用的对象。

1    String str = new String("abc");
2    WeakReference<String> weakReference = new WeakReference<>(str);
3    str = null;
4
5	// JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:
6    str = null;
7    System.gc();

如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用Weak Reference来记住此对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象垃圾回收Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用

虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。

虚引用软引用弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

1    String str = new String("abc");
2    ReferenceQueue queue = new ReferenceQueue();
3    // 创建虚引用,要求必须与一个引用队列关联
4    PhantomReference pr = new PhantomReference(str, queue);