Java基础(5) 泛型 日期和时间 线程 File-输入流

泛型

java的泛型有点像ts的泛型

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}
// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();
  • 泛型就是编写模板代码来适应任意类型;
  • 泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;
  • 注意泛型的继承关系:可以把ArrayList向上转型为List(T不能变!),但不能把ArrayList向上转型为ArrayList(T不能变成父类)。
使用泛型

使用ArrayList的时候,不指定类型,默认就是object;
泛型接口

public interface Comparable<T> {
    /**
     * 返回负数: 当前实例比参数o小
     * 返回0: 当前实例与参数o相等
     * 返回正数: 当前实例比参数o大
     */
    int compareTo(T o);
}

想要使用Array.sorts,放入里面的元素就需要实现comapreTo方法。

class Person implements Comparable<Person> {
    String name;
    int score;
    Person(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
    public String toString() {
        return this.name + "," + this.score;
    }
}

自己编写

class Pari<T> {
    private T first;
    private T last;
    public Pari(T first, T last){
        this.first = first;
        this.last = last;
    }
    public T getFirst(){
        return this.first;
    }
    // 编译报错
    public static Pari<T> create (T first, T last){
        return new Pari<T>(first, last);
    }

}

不能在静态方法上面添加泛型,静态泛型方法应该使用其他类型区分

 // 编译报错
    public static <K>Pari<K> create(K first, K last) {
        return new Pari<K>(first, last);
    }
擦拭法

泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。
Java语言的泛型实现方式是擦拭法(Type Erasure)。
就是,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
当我们编写的泛型,实际上到虚拟机执行的时候

public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
}
  • 编译器把类型视为Object;
  • 编译器根据实现安全的强制转型。

Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

了解java泛型的实现,才能知道它的局限性,

  • T不能是基本类型,比如int,因为实际编译是object,object无法持有基本类型。
  • 无法取得带泛型的Class,因为T是Object,我们对Pair< String>和Pair< Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。也就是说,无论T是啥,getClass都会返回一个class实力,因为编译后它们都属于Pair< Object>
  • 无法判断带泛型的类型:并不存在Pair< String>.class,而是只有唯一的Pair.class
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>) {
}

所以无法通过实例p来获取泛型T

  • 局限四:不能实例化T类型:
public class Pair<T> {
    private T first;
    private T last;
    public Pair() {
        // Compile error:
        first = new T(); //  本意是想创建String,但编译后 first = new Object();
        last = new T(); //last = new Object();
    }
}

创建new Pair< String>()和创建new Pair< Integer>()就全部成了Object
可以通过反射

public class Pair<T> {
    private T first;
    private T last;
    public Pair(Class<T> clazz) {
      this.first = clazz.newInstance();
      this.last = clazz.newInstance()
    }
}
Pair<String> pair = new Pair<>(String.class);

通过String的反射获取到构造函数,然后调用创建一个新的String实例。

  • 泛型方法要防止重复定义方法,例如:public boolean equals(T obj);
  • 虽然,实例p无法获取创建时候传入的T,但是子类可以获取父类的泛型类型。
java的泛型继承
 static int add(Pari<? extends Number> p){
         Number first = p.getFirst();
         Number second = 2;
         return first.intValue()  + second.intValue();
    }

在java中,使用< ? extends xxx > 来限定传入的值 (上界通配符,泛型类型T的上界限定在Number了),
使用< T extends xxx>来限定T的范围
如上

add(Pari<Integer>)
add(Pari<Number>)

都可以

class Pari<T extends Number> {
    private T first;
    private T last;

    public Pari(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return this.first;
    }
   public void setFirst(T val) {
        this.first = val;
    }


    // 编译报错
    public static <K extends Number> Pari<K> create(K first, K last) {
        return new Pari<K>(first, last);
    }

    static int add(Pari<? extends Number> p) {
        Number first = p.getFirst();
        Number second = 2;
        return first.intValue() + second.intValue();
    }

}

这里要注意,当我们使用? extends Number获取到对应的实例的时候,是不能调用对应的set方法的
在这里插入图片描述

原因还是在于擦拭法,当我们传入的p是Pari< Double>时,他满足 ? extends Number,但是他的setFirst显然无法接受Ingeter类型。这就是? extends Number的一个重要限制。
方法参数签名setFirst(? extends Number)无法传递任何Number的子类型给setFirst(? extends Number)。
唯一的例外是可以给方法参数传入null。

作用

当我们需要实现要一个只读接口的时候,就可以通过这种方式。

public interface List<T> {
    int size(); // 获取个数
    T get(int index); // 根据索引获取指定元素
    void add(T t); // 添加一个新元素
    void remove(T t); // 删除一个已有元素
}

int sumOfList(List<? extends Integer> list) {
    int sum = 0;
    for (int i=0; i<list.size(); i++) {
        Integer n = list.get(i);
        sum = sum + n;
    }
    return sum;
}

这里定义的? extends Integer跟直接定义Integer是一样的,但是? extends Integer有个好处,

  • 允许调用get()方法获取Integer的引用;
  • 不允许调用add(? extends Integer)方法并传入任何Integer的引用(null除外)。

所以

  • 使用类似<? extends Number>通配符作为方法参数时表示:
    • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();;
    • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。
    • 使用extends通配符表示可以读,不能写。
super

上面的extends限制了T的上限,比如 ? extends Number,那么只能传入Number和Number的子类。
那如果想限制下限呢,比如 限制只能传入Integet和 integer的父类,可以用super

  static void setFirst1(Pari<? super Integer> p, Integer val1) {
         // void Pari.setFirst(Object val)
        p.setFirst(Integer.valueOf(123));
        // Object Pari.getFirst()
        p.getFirst();
    }

? super Integer表示,方法参数接受,所有泛型类型为Integer或Integer父类的 Pari类型。
此时setFirst也接受Integer及以上的类型的值。

考察Pair< ? super Integer>的setFirst方法,我们传入了? super Integer给 T,那么setFirst,getFirst就是

void setFirst(? super Integer)
? super Integer getFirst();

所以setFirst可以接受Integer以上的值,但是不能用Integer去接受getFirst的值,因为如果传入了Number,那么无法将Number转为Integer(Number即使是抽象类,这里也不能通过编译)
唯一可以接收getFirst()方法返回值的是Object类型

Object obj = p.getFirst();

这些点恰好和extends相反。
所以,? super Integer表示

  • 允许调用set(? super Integer)方法传入Integer的引用;
  • 不允许调用get()方法获得Integer的引用

换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。

extends 和 super的区别

? extends T 允许 调用读方法 T get()获取T的引用,但不允许set(T)传入T的引用。
? super T 允许调用set(T)传入T的引用,不允许调用 T get()获取T的引用(获取object除外)

案例 collections的copy,复制集合

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

这个copy()方法的定义就完美地展示了extends和super的意图:

  • copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
  • copy()方法内部也不会修改src,因为不能调用src.add(T)。

这个copy()方法的另一个好处是可以安全地把一个List< Integer>添加到List< Number>,但是无法反过来添加

  • 因为? super T 表示 ? super Integer, 表示 dest可以写入Integer以上的值,比如Number
  • ? extends T 表示 ? extends Number,表示src可以读取Number一下的值,比如Integer
  • 然后将读取的Integer写入到 add( ? super Integer)中。
  • 但如果反过来则不行了, ? super Number只允许写入Number以上的,Integer显然不行。
PECS原则

Producer Extends Consumer Super
如果需要返回T,它是生产者(Producer),要使用extends通配符;
如果需要写入T,它是消费者(Consumer),要使用super通配符。
如上述的src的生产者,dest的消费者。
需要返回T的src是生产者,因此声明为List<? extends T>,需要写入T的dest是消费者,因此声明为List<? super T>。

无限定通配符
void sample(Pair<?> p) {
}

既不是extends也不是super,所以她不能读,也不能写,只能做一些null判断

static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}

Pair<?>是所有Pair的超类

  Pair<Integer> p = new Pair<>(123, 456);
        Pair<?> p2 = p; // 安全地向上转型
        System.out.println(p2.getFirst() + ", " + p2.getLast());

日期和时间

System.currentTimeMillis(),这是Java程序获取时间戳最常用的方法。

标准库API

我们再来看一下Java标准库提供的API。Java标准库有两套处理日期和时间的API:

  • 一套定义在java.util这个包里面,主要包括Date、Calendar和TimeZone这几个类;
  • 一套新的API是在Java 8引入的,定义在java.time这个包里面,主要包括LocalDateTime、ZonedDateTime、ZoneId等。
// 获取当前时间:
        Date date = new Date();
        System.out.println(date.getYear() + 1900); // 必须加上1900
        System.out.println(date.getMonth() + 1); // 0~11,必须加上1
        System.out.println(date.getDate()); // 1~31,不能加1
        // 转换为String:
        System.out.println(date.toString());
        // 转换为GMT时区:
        System.out.println(date.toGMTString());
        // 转换为本地时区:
        System.out.println(date.toLocaleString());
        // 想要针对用户的偏好精确地控制日期和时间的格式
        var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(sdf.format(date)); //2024-03-02 11:32:47

Date对象有几个严重的问题:它不能转换时区,除了toGMTString()可以按GMT+0:00输出外,Date总是以当前计算机系统的默认时区为基础进行输出。此外,我们也很难对日期和时间进行加减,计算两个日期相差多少天,计算某个月第一个星期一的日期等。

Calendar

Calendar可以用于获取并设置年、月、日、时、分、秒,它和Date比,主要多了一个可以做简单的日期和时间运算的功能。

TimeZone

Calendar和Date相比,它提供了时区转换的功能

新的一套api
  • 本地日期和时间:LocalDateTime,LocalDate,LocalTime;
  • 带时区的日期和时间:ZonedDateTime;
  • 时刻:Instant;
  • 时区:ZoneId,ZoneOffset;
  • 时间间隔:Duration。
  • 以及一套新的用于取代SimpleDateFormat的格式化类型DateTimeFormatter。
   LocalDate d = LocalDate.now(); // 2024-03-02
        LocalTime t = LocalTime.now(); // 11:36:39.612
        LocalDateTime dt = LocalDateTime.now(); //2024-03-02T11:36:39.612

由于执行时机的相差,上述的时间可能对不是,毫秒数。

LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
LocalDate d = dt.toLocalDate(); // 转换到当前日期
LocalTime t = dt.toLocalTime(); // 转换到当前时间

// 指定日期和时间:
LocalDate d2 = LocalDate.of(2019, 11, 30); // 2019-11-30, 注意11=11月
LocalTime t2 = LocalTime.of(15, 16, 17); // 15:16:17
LocalDateTime dt2 = LocalDateTime.of(2019, 11, 30, 15, 16, 17);
LocalDateTime dt3 = LocalDateTime.of(d2, t2);

LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
LocalDate d = LocalDate.parse("2019-11-19");
LocalTime t = LocalTime.parse("15:16:17");

ISO 8601规定的日期和时间分隔符是T。标准格式如下:
日期:yyyy-MM-dd
时间:HH:mm:ss
带毫秒的时间:HH:mm:ss.SSS
日期和时间:yyyy-MM-dd’T’HH:mm:ss
带毫秒的日期和时间:yyyy-MM-dd’T’HH:mm:ss.SSS

DateTimeFormatter
      // 自定义格式化:
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        System.out.println(dtf.format(LocalDateTime.now()));

        // 用自定义格式解析:
        LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf);
        System.out.println(dt2);

此外localDateTime还提供了简单的加减法。

   LocalDateTime dt = LocalDateTime.of(2019, 10, 26, 20, 30, 59);
        System.out.println(dt);
        // 加5天减3小时:
        LocalDateTime dt2 = dt.plusDays(5).minusHours(3);
        System.out.println(dt2); // 2019-10-31T17:30:59
        // 减1月:
        LocalDateTime dt3 = dt2.minusMonths(1);
        System.out.println(dt3); // 2019-09-30T17:30:59

注意到月份加减会自动调整日期,例如从2019-10-31减去1个月得到的结果是2019-09-30,因为9月没有31日。
对日期和时间进行调整则使用withXxx()方法,例如:withHour(15)会把10:11:12变为15:11:12:

调整年:withYear()
调整月:withMonth()
调整日:withDayOfMonth()
调整时:withHour()
调整分:withMinute()
调整秒:withSecond()

  LocalDateTime dt = LocalDateTime.of(2019, 10, 26, 20, 30, 59);
        System.out.println(dt);
        // 日期变为31日:
        LocalDateTime dt2 = dt.withDayOfMonth(31);
        System.out.println(dt2); // 2019-10-31T20:30:59
        // 月份变为9:
        LocalDateTime dt3 = dt2.withMonth(9);
        System.out.println(dt3); // 2019-09-30T20:30:59

线程

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

和多线程相比,多进程的缺点在于:
  • 创建进程比创建线程开销大,尤其是在Windows系统上;
    进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
    而多进程的优点在于:

  • 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

Java多线程编程的特点又在于:

多线程模型是Java程序最基本的并发模型;
后续读写网络、数据库、Web开发等都依赖Java多线程模型。

创建一个线程

java通过new Thread
或者一个Thread的子类
或者实现Runnable接口的类来创建一个线程,
通过start启动一个线程

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("start new thread1");
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread2");
    }
}

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("main start");
        Thread t = new MyThread("test"); //参数是线程名称
        // start()方法会在内部自动调用实例的run()方法
        t.start(); // 开始并发执行
        Thread t2 = new Thread(new MyRunnable());
        t2.start();
        Thread t3 = new Thread() {
            public void run() {
                System.out.println("start new thread3");
            }
        };
        t3.start();
        System.out.println("main end");
    }
   }

上述执行结果

main start
start new thread1
main end
start new thread2
start new thread3

main主线程在执行start之后,其他线程就跟主线程开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。
Thread.sleep()可以把当前线程暂停一段时间。

Thread类常用的方法

public void run() 线程相关的代码都写在里面执行
public void start()启动线程
public void join(long ms) t.join(),会等待t线程之行结束再执行其他线程,参数是等待最多的时间,超过这个时间就不等待了
public static void sleep(long m) 调用Thread.sleep可以让当前线程暂停m毫秒。

Runnable接口
  • 只有一个方法
  • Runnable是Java实现线程的接口。
  • 任何实现线程功能的类,都必须实现该接口。
线程状态

一个线程对象只能调用一次start启动新线程,并在新线程中执行run方法,一旦run方法执行完毕,线程就结束了,因此,java线程的状态有以下几种。

  • New:新创建的线程,尚未执行;
  • Runnable:线程调用了start()方法后进入就绪状态,并且当线程调度器为其分配CPU资源时,它会转为运行状态。在Java中,这个状态包括了“就绪”和“运行”两种情况,也就是说,线程可能正在执行,也可能准备好了随时可以执行,只是当前没有获得CPU时间片。
  • Blocked: 阻塞状态 运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

在这里插入图片描述
t.join()可以等到t线程结束后再执行主线程。

线程优先级
  • java提供了十个优先级,1-10,主线程优先级默认为5,
  • 优先级常量 MAX_PRIORITY 最高优先级10,MIN_PRIORITY最低优先级1,NORMOR_PRIORITY默认优先级5
  • public int getPriority()获取优先级 public void setPriority(int newPriority)设置优先级
  • JVM自动把1(低)~10(高)的优先级映射到操作系统实际优先级上(不同操作系统有不同的优先级数量)。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
中断线程

假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

class MyThread extends Thread {
    @Override
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
        }
    }
}

 public static void main(String[] args) throws InterruptedException {
        System.out.println("main start");
        Thread t = new MyThread();
        // start()方法会在内部自动调用实例的run()方法
        t.start(); // 开始并发执行
        Thread.sleep(1); // 暂停1毫秒
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("main end");
    }


另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。

volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。
小结
  • 对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;
  • 目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;
  • 通过标志位判断需要正确使用volatile关键字;
  • volatile关键字解决了共享变量在线程间的可见性问题。
守护线程

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

因此,JVM退出时,不必关心守护线程是否已结束。
setDaemon 标记为守护线程

Thread t = new MyThread();
t.setDaemon(true);
t.start();
多线程运行问题
  • 各个线程是通过竞争CPU时间获得运行机会的。
  • 各个线程在什么时候得到CPU时间,占用多久,是不可预测的。
  • 一个正在运行着的线程在什么地方被暂停是不确定的。
线程同步

如果有两个线程,比如银行系统,他有一个Bank对象,一个线程用来存,一个线程用来取,因为线程的操作时机是不确定的,就会导致存取时候得到的银行余额并不是最新的,导致出错。
为了解决这个问题,就需要将Bank对象锁住

使用synchronized关键字。

public synchornized void set(){}
public static synchornized void set(){}
synchornized(obj){setxx}

多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
在这里插入图片描述
通过加锁解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。
这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {
    n = n + 1;
}


public class Main {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock = new Object();
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count += 1;
            }
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count -= 1;
            }
        }
    }
}

注意·

synchronized(Counter.lock) { // 获取锁
    ...
} // 释放锁
不需要synchronized的操作
  • 原子操作不需要synchronized操作,比如基本类型赋值(long,double除外)int n = m; 引用类型赋值List< string> list = anthoerList;
  • 单条原子操作语句不需要赋值
  • 多行赋值语句,就必须保证是同步操作
  • 如果多线程读写的是一个不可变对象,那么无需同步,因为不会修改对象的状态
小结
  • 多线程同时读写共享变量时,可能会造成逻辑错误,因此需要通过synchronized同步;
  • 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
  • 注意加锁对象必须是同一个实例;
  • 对JVM定义的单个原子操作不需要同步。
同步方法

自己写synchronized,还得注意需要锁住同一个对象,更好的方法是把synchronized封装起来

public class Counter {
    private int count = 0;

    public void add(int n) {
        synchronized(this) {
            count += n;
        }
    }
		// 等同于
	public synchronized void add(int n) { // 锁住this
    count += n;
		} // 解锁

    public void dec(int n) {
        synchronized(this) {
            count -= n;
        }
    }

    public int get() {
        return count;
    }
}

用synchronized修饰的方法就是同步方法,他会将当前的this实例锁起来。

线程调用add()、dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()、dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行

如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的Counter类就是线程安全的。Java标准库的java.lang.StringBuffer也是线程安全的。

如果用synchronized修饰static,static没有this,但是JVM会给每个类创建一个Class 实例,所以用synchronized修饰static方法,锁住的是Class实例

public class Counter {
    public static void test(int n) {
        synchronized(Counter.class) {
            ...
        }
    }
}
线程通信
  • wait() 中断方法的执行,使线程等待
  • notfify方法,唤醒处于等待的某一个线程,使其结束等待
  • notifyall方法,唤醒所有处于等待的线程,使他们结束等待。

输入输出流

输出流:

流就是指一串流动的字符,以先进先出的方式发送信息的通道。
System.out.println("test")
输出流就是程序进行写操作,将字符串"test" 以 t. e. s. t的形式一个一个通过通道塞到目的地,而这个通道就是流的形式。
更多的操作比如 打印文件也是通过流的方式。

输入流

程序通过流的形式读取数据,比如键盘输入数据,程序通过流的形式一个一个读取数据,比如读取文件,也是通过流的形式。
输入对应读取
输出对应写入

File类

Java的标准库java.io提供了File对象来操作文件和目录。

文件或文件目录

构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。

 // file
        // File file = new File("./test.txt");
        // File file = new File("/Users/test/Desktop/java/test.txt");
        File file1 = new File("/Users/test");
        File file = new File(file1, "Desktop/java/test.txt");
        System.out.println("是否文件" + file.isFile());
        System.out.println("是否目录" + file.isDirectory());
 		System.out.println(file.getPath()); // 传入的路径
        System.out.println(file.getAbsolutePath()); // 绝对路径
        try {
            System.out.println(file.getCanonicalPath()); // 规范路径
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // 创建目录
        File file2 = new File("./test/test2");
        System.out.println("test2存在?" + file2.exists());
        if (!file2.exists()) {
            // 不存在则创建目录
            file2.mkdirs();// 多层级用mkdirs,单层用mkdir

        }

        // 创建文件 createNewFile
        File file3 = new File("./test2.txt");
        if (!file3.exists()) {
            try {
                file3.createNewFile();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

new File可以传入绝对路径,也可以传入相对路径。
file.exists()判断文件是否存在
file.isDirector判断file是否目录
file.isFile()判断file是否文件
file.mkdir[s]创建file目录,+s表示多层目录
file.createNewFile()创建file文件
file.getPath() 传入的路径
file.getAbsolutePath() 绝对路径
file.getCanonicalPath 规范路径
boolean canExecute():是否可执行;
long length():文件字节大小。
file.delete() 删除文件,成功返回true

注意Windows平台使用\作为路径分隔符,在Java字符串中需要用\表示一个\。Linux平台使用/作为路径分隔符

// 假设当前目录是C:\Docs
File f1 = new File("sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File("..\\sub\\javac"); // 绝对路径是C:\sub\javac
规范路径
 File f = new File("..");
        System.out.println(f.getPath());
        System.out.println(f.getAbsolutePath());
        try {
            System.out.println(f.getCanonicalPath());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

打印结果

..
/Users/test/Desktop/java/..
/Users/test/Desktop

可以看到,绝对路径就是/Users/test/Desktop/java/…,规范路径就是把.和…转换成标准的绝对路径后的路径
/Users/test/Desktop/java/..实际上就是/Users/test/Desktop

打印系统分隔符

System.out.println(File.separator); // 根据当前平台打印"“或”/"

其他文件操作

程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。

  public static void main(String[] args) throws IOException {
        File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
        f.deleteOnExit(); // JVM退出时自动删除
        System.out.println(f.isFile());
        System.out.println(f.getAbsolutePath());
    }
遍历文件和目录

当File对象表示一个目录时,可以使用list()和listFiles()列出目录下的文件和子目录名。

 public static void main(String[] ages) {
        File f = new File("./");
        String[] fs1 = f.list();
        System.out.println(Arrays.toString(fs1));
        // printFiles(fs1);
        File[] fs2 = f.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".txt");
            }
        });
        printFiles(fs2);
    }

    static void printFiles(File[] files) {
        System.out.println("==========");
        if (files != null) {
            for (File f : files) {
                System.out.println(f);
            }
        }
        System.out.println("==========");
    }

file.list()获取当面文件目录的名称,返回字符串数组,即使是目录也会返回。
file.listFiles可以遍历当前文件目录下的所有文件/文件夹,参数可以传入FilenameFilter实例,过滤数据。

Path

Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单:

 Path p1 = Paths.get(".", "text.txt"); // 构造一个Path对象 
        System.out.println(p1); //传入的路径
        Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
        System.out.println(p2);
        Path p3 = p2.normalize(); // 转换为规范路径
        System.out.println(p3);
        File f = p3.toFile(); // 转换为File对象
        System.out.println(f.isFile());

结果就是

./text.txt
/Users/test/Desktop/java/./text.txt
/Users/test/Desktop/java/text.txt
/Users/test/Desktop/java/text.txt

p1 传入的路径
p1.toAbsolutePath()转为绝对路径
p1.normalize转为规范路径
p1.toFile转为file对象

如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。

小结

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

  • 创建file对象不涉及IO操作
  • 通过file.getPath, getAbsoultePath(), getCanonicalPath() 可以获取传入的路径,绝对路径和规范路径
  • file.lists() 获取当前目录下的所有文件/文件夹的名称,返回数组,file.listFiles返回目录下的所有file对象
  • 可以创建或删除文件和目录。
InputStream 输入流

InputStream就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。
InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read()
public abstract int read() throws IOException;
这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。

FileInputStream

FileInputStream是InputStream的子类

public void readFile() throws IOException {
     try {
            InputStream input = new FileInputStream("./test.txt");
            for (;;) {
                int n;
                n = input.read();
                if (n == -1) {
                    break;
                }
                System.out.println(n); // 打印byte的值

            }
         
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
          if (input != null) { input.close(); }
        }
}

文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出
所有与IO操作相关的代码都必须正确处理IOException

用try … finally来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源。

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

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

缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。

InputStream提供了两个重载方法来支持读取多个字节:

int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填

read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。

 try (InputStream input = new FileInputStream("src\\test.txt")) {
            byte[] buffer = new byte[5];
            int n;
            while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
                System.out.println("read" + n + "bytes");
                // read5bytes
				// read5bytes
				// read1bytes
            }
        }

一次读取5个字节

阻塞

read方法读取数据的时候,read方法是阻塞的,他的意见是

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

执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。

InputStream实现类

用FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。此外,ByteArrayInputStream可以在内存中模拟一个InputStream

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] data = { 72, 101, 108, 108, 111, 33 };
        try (InputStream input = new ByteArrayInputStream(data)) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println((char)n);
            }
        }
    }
}

用ByteArrayInputStream,实际上是把一个byte[]数组在内存中变成一个InputStream。
我们稍微改变下

   String s;
        try (InputStream input = new FileInputStream("src\\test.txt")) {
            s = readAsString(input);
        }
        System.out.println(s);

        byte[] data = { 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 };
        try (InputStream input2 = new ByteArrayInputStream(data)) {
            String s2 = readAsString(input2);
            System.out.println(s2);
        }



   public static String readAsString(InputStream input) throws IOException {
        int n;
        StringBuilder sb = new StringBuilder();
        while ((n = input.read()) != -1) {
            System.out.print(n + "\n");
            sb.append((char) n);
        }
        return sb.toString();
    }

结果都是hello world
因为接受的是InputStream抽象类,所以所有实现InputStream的类都能传入。这就是面向对象编程原则的应用。

  • 23
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值