泛型
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的类都能传入。这就是面向对象编程原则的应用。