JAVA入门基础_JAVA IO流、多线程、集合(四)

目录

IO流

什么是IO流、能够做什么、分类

  • 什么是IO流?
 IO流中的I为Input、其中的O为Output,翻译过来为:输入输出流
 
 其中的流是将 输入/输出的设备(键盘、打印机、文件等)抽象的形容为流,通过流的形式来完成**对数据的传输与接收**
  • 能够做什么?
    一句话:主要是用于处理数据的传输。

  • IO流的分类

    • 按照流的方向:输入流(用来读的)、输出流(用来写入的)

    • 按照流的数据单位: 字节流、字符流

    • 按照流的功能:节点流(直接与数据源相接)、处理流(对节点流进行包装)

IO的基本使用

节点流的基本使用FileInputStream、FileOutputStream、FileReader、FileWriter

FileInputStream、FileOutputStream的基本读写(操作的是字节)

/**
 * @author codeStars
 * @date 2022/7/26 9:54
 */
public class Calculate {
    public static void main(String[] args) throws Exception{

        // 输入流指定一个文件(读)
        InputStream is = new FileInputStream("d:/test/a.txt");
        // 输出流指定一个文件(写)
        OutputStream fs = new FileOutputStream("d:/test/b.txt");

        // 创建一个字节数组,用于存储输入流读取的数据
        byte[] buf = new byte[1024];

        // 创建一个整形,用于存储每次读取多少了个字节
        int len = 0;

        // 先将数据读取到字节数组buf中,并且将读取的字节数量赋值给len。
        // read方法,会返回读取了多少个字节,
        //        如果读取到文件结尾,则返回-1,因此while循环则判断每次写入时返回的结果是否为-1
        while ((len = is.read(buf)) != -1) {
            // 使用输出流节点流将数据写入到"d:/test/b.txt" 文件中,
                // 一共有3个参数,第一个参数:写入的字节数组。第二个参数:从字节数组的第多少位开始写入数据。第三个参数:写入多少个字节
            fs.write(buf, 0, len);
        }

        // 关闭流、要从下至上的关闭,一般写在finally语句块中
        fs.close();
        is.close();
    }
}

FileReader、FileWriter的基本读写(操作的是字符)

public class Calculate {
    public static void main(String[] args) throws Exception{

        // 输入流指定一个文件(读)
        FileReader fr = new FileReader("d:/test/a.txt");
        // 输出流指定一个文件(写)
        FileWriter fw = new FileWriter("d:/test/b.txt");


        // 创建一个字符数组,用于存储输入流读取的数据
        char[] buf = new char[1024];

        // 创建一个整形,用于存储每次读取多少了个字符
        int len = 0;

        // 先将数据读取到字符数组buf中,并且将读取的字符数量赋值给len。
        // read方法,会返回读取了多少个字符,
        //        如果读取到文件结尾,则返回-1,因此while循环则判断每次写入时返回的结果是否为-1
        while ((len = fr.read(buf)) != -1) {
            // 使用输出流节点流将数据写入到"d:/test/b.txt" 文件中,
                // 一共有3个参数,第一个参数:写入的字符数组。第二个参数:从字符数组的第多少位开始写入数据。第三个参数:写入多少个字符
            fw.write(buf, 0, len);
        }

        // 关闭流、要从下至上的关闭,一般写在finally语句块中
        fw.close();
        fr.close();
    }
}

FileInputStream、FileOutputStream、FileReader、FileWriter的区别与使用场景

  • FileInputStream与FileOutputstream是字节输入输出流,可以用于读写任何文件

  • FileReader、FileWriter是字符输入输出流,只能用于读取文本文件

流的close()方法、flush()方法及使用时机
  • close()方法: 关闭流,就是把流跟数据源的连接切断。当流使用完毕时使用,放在finlly语句块中

  • flush()方法: 将缓冲区中的数据写入到源文件中。

    • 当使用处理流时使用,因为节点流是直接与源文件所连接的,所以会直接将数据写入到源文件中。

    • 当使用字符输出流时,一定要想到flush()方法。

常用的处理流(包装节点流的一种流)

缓冲输入、输出流

BufferedInputStream、BufferedOutputStream字节缓冲、输入流
public class Calculate {
    public static void main(String[] args) throws Exception{
        // 缓冲输入流指定一个文件(读)
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("d:/test/a.txt"));
        // 缓冲输出流指定一个文件(写)
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("d:/test/b.txt"));


        // 创建一个字节数组,用于存储输入流读取的数据
        byte[] buf = new byte[1024];

        // 创建一个整形,用于存储每次读取多少了个字节
        int len = 0;

        // 先将数据读取到字节数组buf中,并且将读取的字节数量赋值给len。
        // read方法,会返回读取了多少个字节,
        //        如果读取到文件结尾,则返回-1,因此while循环则判断每次写入时返回的结果是否为-1
        while ((len = bis.read(buf)) != -1) {
            // 使用输出流节点流将数据写入到"d:/test/b.txt" 文件中,
            // 一共有3个参数,第一个参数:写入的字节数组。第二个参数:从字节数组的第多少位开始写入数据。第三个参数:写入多少个字节
            bos.write(buf, 0, len);
        }

        // 关闭流、要从下至上的关闭,一般写在finally语句块中
        bos.close();
        bis.close();
    }
}
BufferedReader、BufferedWriter字符缓冲流
public class Calculate {
    public static void main(String[] args) throws Exception{
        // 这种readLine(),write()写入的复制方式,一般复制的文件结尾会多一个换行。

        // 缓冲输入流指定一个文件(读)
        BufferedReader fr = new BufferedReader(new FileReader("d:/test/a.txt"));
        // 缓冲输出流指定一个文件(写)
        BufferedWriter fw = new BufferedWriter(new FileWriter("d:/test/b.txt"));

        // 定义一个字符串用于接收每一行的文本数据
        String line = null;

        // readLine()方法,若是读取到文件结尾则会返回null
        while ((line = fr.readLine()) != null) {
            // 将读取的一行数据写入到复制的文件中
            fw.write(line);
            // 添加一个换行,(因为readLine方法读取时不会读取换行符)
            fw.newLine();
        }

        // 关闭流、要从下至上的关闭,一般写在finally语句块中
        fw.close();
        fr.close();
    }
}
缓冲流中的数据写入文件的时机、字符缓冲流的readLine()、newLine()方法
  • 缓冲流只有执行了其flush()方法后,才会把缓冲区中的数据写入到源文件中

  • 当执行了close()方法后,底层会自动调用一次flush()方法将数据写入到源文件中

  • BufferedReader类中的 readLine():读取一行文本数据,但是不包含换行符

  • BufferedWriter类中的 newLine():手动添加一个换行符

输入缓冲流和输出缓冲流同时定义时,文件会被直接清空的问题
  • 定义输出流时,传入的节点流可以开启append

DataInputStream、DataOutputStream 基本类型输入输出流

public class Calculate {

    public static void main(String[] args) throws Exception{
        DataInputStream dis = new DataInputStream(new FileInputStream("d:/test/a.txt"));
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/test/a.txt"));

        // 使用数据字节输出流向一个文件写入数据
        dos.writeInt(10);
        dos.writeUTF("今天天气不错");
        dos.writeByte(20);
        dos.writeBoolean(true);


        // 使用数据字节输入流读取一个文件的数据,注意读的时候必须按照写入的顺序读,不然读取不到正确的值
        System.out.println(dis.readInt());
        System.out.println(dis.readUTF());
        System.out.println(dis.readByte());
        System.out.println(dis.readBoolean());

        // 关闭流
        dos.close();
        dis.close();
    }
}

ObjectInputStream、ObjectOutputStream对象输入输出流

作用及使用条件
  • 作用:用于写入对象及读取对象

  • 使用条件:需要被写入的对象必须实现Serializable序列化接口才能被序列化,并且可以通过idea工具生成一个serialVersionUID。

transient修饰符(修饰成员变量)

当成员变量被transient修饰时,该变量将不会被序列化。

对象流的基本使用方法
public class Calculate {
    public static void main(String[] args) throws Exception{

        // 对象输出流,用于向文件中写入对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/test/a.txt"));

        oos.writeObject(new Student("张三", 12, 25));
        oos.writeObject(new Student("李四", 13, 26));


        // 对象输入流,用于从文件中读取对象
        // 注意,ObjectInputStream 所指定的文件字节大小不能为0,否则将会抛出EOFException异常
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/test/a.txt"));

        // 取出2个对象,注意顺序:会按照对象的存入顺序取出
        Student s1 = (Student) ois.readObject();
        Student s2 = (Student) ois.readObject();

        // 结果为:Student{name='张三', age=12, tAge=0}
        System.out.println(s1);  
        // 结果为:Student{name='李四', age=13, tAge=0}
        System.out.println(s2);
        
        // 关闭流
        oos.close();
        ois.close();
    }
}

PrintWriter、PrintStream 字符/字节打印流(均为输出流)

public class Calculate {
    public static void main(String[] args) throws Exception {

        // 创建一个字节打印流,指定输出位置为控制台
        PrintStream ps = new PrintStream(System.out);
        ps.print("aa"); // 不用flush

        // 创建一个字符打印流,指定输出位置为控制台
        PrintWriter pw = new PrintWriter(System.out);
        pw.print("bb");
        pw.flush(); // 需要flush

        // 关闭流
        pw.close();
        ps.close();
    }

}

InputStreamReader、OutputStreamWriter 转换流(只有字节流转字符流)

public class Calculate {
    public static void main(String[] args) throws Exception {

        // 创建2个字节流,分别为一个输出流一个输入流
        FileInputStream fis = new FileInputStream("d:/test/a.txt");
        FileOutputStream fos = new FileOutputStream("d:/test/b.txt");

        // 将字节输入、输出流转换为字符输入、输出流
        InputStreamReader isr = new InputStreamReader(fis);
        OutputStreamWriter osw = new OutputStreamWriter(fos);

        // 关闭流
        osw.close();
        isr.close();

        // 注意事项: 不是说转换成了字符流,这个字节流就可以关闭了,转换后的字符流也还在用着该字节流
        fos.close();
        fis.close();
    }
}
转换流的妙用
  • 想想 System.out 这个字节流

  • 可以把这个字节流转换为字符流来中(非常好用)

PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));

pw.println("adfsaf");
pw.flush();
被转换的字节流不能关闭

如果关闭了,那么则会导致被转换后的字符流在进行数据读写时抛出IOException: Stream Closed的异常。

使用IO流配合File类实现文件夹的复制

public class IOCopyDirectory {
    public static void main(String[] args) throws IOException {
        // 1.源文件夹
        String sourceDir = "d:/test";
        // 2.目标文件夹
        String destDir = "d:/test2";

        // 3.复制文件夹
        copyDirectory(sourceDir, destDir);
    }


    public static void copyDirectory(String sourceDir, String destDir) throws IOException {
        // 1. 使用2个File对象封装目录
        File sourceDirFile = new File(sourceDir);
        File destDirFile = new File(destDir);

        // 2. 判断源目录是否存在,不存在则抛出异常
        if (!sourceDirFile.exists()) {
            throw new IllegalArgumentException("源路径不存在");
        }

        // 3. 创建目标文件夹
        destDirFile.mkdirs();

        // 4. 循环源路径下的所有文件及文件夹
        for (File file : sourceDirFile.listFiles()) {
            // 4.1 如果是一个文件,则复制文件
            if(file.isFile()) {
                copyFile(file.getAbsolutePath(), destDirFile.getAbsolutePath() + File.separator + file.getName());
            }else {
                // 4.2 如果不是文件,就代表是一个目录,递归调用该方法
                copyDirectory(file.getAbsolutePath(), destDirFile.getAbsolutePath() + File.separator + file.getName());
            }
        }
    }

    /**
     * 复制文件
     * @param source 文件的源地址
     * @param dest 文件的目标地址
     */
    public static void copyFile(String source, String dest) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            bis = new BufferedInputStream(new FileInputStream(source));
            bos = new BufferedOutputStream(new FileOutputStream(dest));

            int len = 0;
            byte[] buf = new byte[1024];

            // 复制文件
            while ((len = bis.read(buf)) != -1) {
                bos.write(buf, 0, len);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null) {
                    bis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程

进程与线程的区别

  • 进程就是相当于一个主入口函数,一个运行中的应用程序

  • 一个进程可以拥有多个线程,但是也至少拥有一个线程

  • 线程是一条独立的执行链路

线程的三种创建方式及优缺点

继承Thread类

优点:直接继承Thread类重写run方法,使用简单
缺点:导致子类无法进行其他继承

class ThreadDemo {
    public static void main(String[] args) {
        // 创建一个线程,线程进入创建状态
        Thread thread = new ThreadOne();
        // 线程运行,进入就绪状态
        thread.start();
    }

}

class ThreadOne extends Thread{
    @Override
    public void run() {
        System.out.println("我是继承了Thread类的run方法");
    }
}

实现Runnable接口

优点:采用实现接口的方式实现,不影响子类的扩展

class ThreadDemo {
    public static void main(String[] args) {
        // 创建一个线程,线程进入创建状态
        Thread thread = new Thread(new ThreadTwo());
        // 线程运行,进入就绪状态
        thread.start();
    }

}

class ThreadTwo implements Runnable{
    @Override
    public void run() {
        System.out.println("我是实现了Runnable接口的run方法");
    }
}

实现Callable接口

优点:

  • 采用实现接口的方式实现,不影响子类扩展
  • 该线程可以有返回值。
class ThreadDemo {
    public static void main(String[] args) throws Exception{
        // 获取到一个FutureTask对象,用于获取线程的返回值
        FutureTask<String> futureTask = new FutureTask(new ThreadThree());

        // 创建一个线程,把FutureTask对象放进去
        // 为啥能放呢? FutureTask implements RunnableFuture,RunnableFuture extends Runnable
        // Thread的构造函数可以放入一个Runnable对象,所以可以放进去
        Thread thread = new Thread(futureTask);

        // 运行线程
        thread.start();

        // 获取到线程的返回值
        String str = futureTask.get();

        // 输出线程的返回值
        System.out.println(str);
    }

}

class ThreadThree implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("我是实现了Callable的一个线程");

        return "我是一个线程,我的返回值就是这个字符串。";
    }
}

线程的5个生命周期

  • 创建状态:当new一个线程时,这个线程就进入了创建状态

  • 就绪状态:当一个线程调用start()方法时,线程就进入了就绪状态,等待cpu分配执行权

  • 运行状态:当线程获取到cpu执行权并运行时,就进入了运行状态

  • 阻塞状态(3种),只有线程在运行状态时可以转换为阻塞状态

    • 同步阻塞(锁池队列):当线程没有拿到时所进入的状态
    • 等待阻塞(阻塞队列):当线程调用或被调用了wait()方法时
    • 其他阻塞:sleep()、join()、scanner以及其他IO阻塞时进入其他阻塞状态
  • 死亡状态(3种方式)

    • 当线程中的run方法运行结束时,线程死亡
    • 当线程被中断时interrupt(),线程死亡
    • 当线程内部运行时抛出了异常,线程死亡

线程控制的5个方法的作用(均为Thread类中定义的方法)

join()

  一个线程A在运行时:执行了线程B的join()方法,那么A线程将会进入其他阻塞状态

等待线程B执行完毕后线程A将会进入就绪状态

yield()

当一个线程执行了yield()方法后,该线程将会从运行状态进入就绪状态,等待cpu分配执行权

wait(),必须在同步代码块或同步方法中使用

一个线程在调用了锁的wait()方法,则会让该线程进入该锁的阻塞队列,并会释放自身拿到的锁。

notify(),必须在同步代码块或同步方法中使用

线程在运行过程中,若是调用了锁的notify方法,则会随机唤醒该锁阻塞队列中的一个线程,使其进入就绪状态。

setDaemon() 将一个线程设置为守护线程

  • 必须在线程start()之前调用该线程的setDaemon()方法

  • 将线程设置为守护线程,会随着开启该线程的线程结束而结束

interrupt(),不推荐使用

  • 将一个线程设置为中断状态

  • 当一个线程为中断状态时,调用该线程的isInterrupted()方法会获取到true

Thread.currentThread()方法

  • 可以获取运行到该方法的线程。返回类型为:Thread

sleep()、wait()方法的作用及区别

  • sleep():被调用该方法的线程会进入其他阻塞状态(不释放锁)

  • wait():被调用该方法的线程会进入等待阻塞(释放锁)

  • wait()方法的调用条件(同步代码块、同步方法),sleep()的调用条件(任何地方)

线程安全问题

什么是线程安全问题、什么时候可能会出现

  • 只有当2个及以上的线程共享同一个引用类型的变量时可能出现线程安全问题

  • 线程安全问题:可能导致原本的数据变成脏数据(跟预期不一致或不合理)

用卖票来演示线程安全问题

public class Test {
    public static void main(String[] args) {
        // 创建3个线程
        Thread t1 = new Thread(new Thread1());
        Thread t2 = new Thread(new Thread1());
        Thread t3 = new Thread(new Thread1());

        // 3个线程同时卖票
        t1.start();
        t2.start();
        t3.start();
    }
}

class Thread1 implements Runnable {
    // 车票数量,设置为static类变量,让所有线程共享
    private static int  ticket = 100;


    @Override
    public void run() {


        // 定义一个死循环,循环的结束由内部控制
        while (true) {
            // 如果车票卖完了,那就不卖了
            if (ticket <= 0) {
                break;
            }

            if (ticket > 0) {
                try {
                    // 让当前线程在刚判断到票大于0时睡眠一会,若是当票刚好为1时,另外一个线程进来了,
                        // 那么现在只有一张票,2个线程都要卖,也就出现脏数据了。
                    Thread.sleep(10);

                    // 输出一下当前剩下多少票
                    System.out.println(Thread.currentThread().getName() + "--卖出了一张票,现在还剩" + --ticket  + "张票");

                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

image
从如上的运行结果可以看到,出现了很多的脏数据。

解决线程安全的方法

同步代码块

在线程所需要执行的代码当中,添加如下代码块可以达到锁的效果:

synchronized (锁) {
            System.out.println("啦啦啦");
}

锁的注意事项:

  1. 建议使用final修饰

  2. 锁的引用不可修改,否则无法达到锁的效果

  3. 锁只能是引用类型,不能是基础类型。可以是this也能是任意类的.class

同步方法(不推荐)

在线程调用的方法上加上synchronized也可以实现加锁的效果。

如果是成员方法:使用的是this锁,也就是当前对象

如果是静态方法:使用的是当前类的.class

    public synchronized void show() {
        System.out.println("我是一个加了锁的成员方法,使用的是this锁");
    }

    public static synchronized void show2() {
        System.out.println("我是一个加了锁的静态方法,使用的是当前类的class锁");
    }

Lock锁(JDK1.5之后出的)

  • 该锁可以显式的进行加锁、解锁

  • 该锁可以创建多个阻塞队列(也就是等待阻塞状态所进入的队列,提示下:使用wait进入的队列)

// 1. 创建一个Lock锁,这里new的是它的一个子类:可重入的独占锁
Lock lock = new ReentrantLock();

// 2. 创建2个阻塞队列
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();

// 3. 加锁与解锁的方法
lock.lock(); // 加锁
lock.unlock(); // 解锁

// 4. 让线程进入指定的阻塞队列
condition1.await();

// 5. 唤醒指定阻塞队列中的线程
condition2.signal();

解决线程安全问题

使用同步代码块实现生产者消费者(达到生产一个、消费一个的效果)

/**
 * @author codeStars
 * @date 2022/7/30 16:34
 */
public class CodeChunk {
    public static void main(String[] args) {

        // 1. 创建一个产品,让用于让生产者、消费者共用
        Product product = new Product();

        // 2. 定义生产者和消费者,并且传入同一个产品对象
        Thread producer = new ProducerThread(product);
        Thread consumer = new ConsumerThread(product);

        // 3. 运行生产者与消费者
        producer.start();
        consumer.start();
    }
}

class Product {
    // 1. 定义一个标记、用于标识当前是否有产品,true为有、false为没有,默认为没有
    private boolean flag;

    // 2. 定义产品的名称
    private String name;

    // 3. 定义产品的颜色
    private String color;

    // 4. 提供下各成员变量的get/set方法
    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}

/**
 * 生产者线程
 */
class ProducerThread extends Thread {
    private Product product;

    public ProducerThread(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        // 定义一个标记,用于每次都创建不同的产品
        int count = 0;

        // 定义循环,无限的生产产品
        while (true) {
            try {
                synchronized (product) {
                    // 1. 如果没有产品,则进行等待,注意:必须跟另一个线程进入同一个阻塞队列
                    if(product.isFlag()){
					    // 让当前线程进入到product锁的阻塞队列
                        product.wait();
                    }

                    // 2. 生产产品
                    if((count & 1) == 0) {
                        product.setName("汽车");
                        product.setColor("黑色");
                    }else {
                        product.setName("大衣");
                        product.setColor("粉色");
                    }
                    // 输出一下生产信息
                    System.out.println("生产者生产了一个产品,名称为: " + product.getName() + ",颜色为:" + product.getColor());

                    // 标记加一
                    count ++;

                    // 3. 修改标记
                    product.setFlag(true);

                    // 4. 唤醒消费者
                    product.notify();
                }

            }catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

/**
 * 消费者线程
 */
class ConsumerThread extends Thread {
    private Product product;

    public ConsumerThread(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        // 定义循环,无限的消费产品
        while (true) {
            try {
                synchronized (product) {
                    // 1. 如果没有产品,则进行等待,注意:必须跟另一个线程进入同一个阻塞队列
                    if (!product.isFlag()) {
						// 让当前线程进入到product锁的阻塞队列
                        product.wait();
                    }

                    // 2. 消费产品,输出一下消费信息
                    System.out.println("消费者消费了一个产品,名称为: " + product.getName() + ",颜色为:" + product.getColor() + "============");

                    // 3. 修改标记
                    product.setFlag(false);

                    // 4. 唤醒生产者
                    product.notify();
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

使用Lock锁实现生产者消费者(达到生产一个、消费一个的效果)

public class LockDemo {
    public static void main(String[] args) {

        // 1. 创建一个产品,让用于让生产者、消费者共用
        Product product = new Product();

        // 2. 定义生产者和消费者,并且传入同一个产品对象
        Thread producer = new ProducerThread(product);
        Thread consumer = new ConsumerThread(product);

        // 3. 运行生产者与消费者
        producer.start();
        consumer.start();
    }
}

class Product {
    // 1. 定义一个标记、用于标识当前是否有产品,true为有、false为没有,默认为没有
    private boolean flag;

    // 2. 定义产品的名称
    private String name;

    // 3. 定义产品的颜色
    private String color;

    // 4. 定义一个锁
    private final Lock lock = new ReentrantLock();

    // 5. 定义2个阻塞队列,分别为生产者、消费者的阻塞队列
    private final Condition producerCondition =  lock.newCondition();
    private final Condition consumerCondition =  lock.newCondition();

    /**
     * 生产产品
     */
    public void producer() {
        int count = 0;
        // 循环无限的生产产品
        while (true) {
            try {
                // 1. 加锁
                lock.lock();

                // 2.如果当前有商品了,生产者等待
                if(flag) {
                    producerCondition.await();
                }

                // 3.每次都生产不同的产品
                if ((count & 1) == 0) {
                    this.name = "电脑";
                    this.color = "黑色";
                }else {
                    this.name = "手机";
                    this.color = "红红红色";
                }
                System.out.println("生产者生产了一个产品,名称为: " + name + ",颜色为:" + color);
                // count + 1
                count ++;

                // 5. 修改标记
                flag = true;

                // 5. 唤醒消费者
                consumerCondition.signal();
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                // 6. 解锁
                lock.unlock();
            }
        }

    }

    // 消费产品
    public void consumer() {
        // 循环无限的生产产品
        while (true) {
            try {
                // 1.加锁
                lock.lock();

                // 2.如果当前没有产品,则等待
                if(!flag) {
                    consumerCondition.await();
                }

                // 3. 消费产品
                System.out.println("消费者消费了一个产品,名称为: " + name + ",颜色为:" + color + "============");

                // 4. 修改标记
                flag = false;

                // 5. 唤醒生产者
                producerCondition.signal();
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                // 6. 解锁
                lock.unlock();
            }

        }
    }
}

/**
 * 生产者线程
 */
class ProducerThread extends Thread {
    private Product product;

    public ProducerThread(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        // 生产产品
        product.producer();
    }
}

/**
 * 消费者线程
 */
class ConsumerThread extends Thread {
    private Product product;

    public ConsumerThread(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        // 消费产品
        product.consumer();
    }
}

集合

什么是集合,集合能干什么

  • 集合实际是就是存储数据的容器

  • 集合可以用于存储相同数据类型的数据

数组也可以存,为什么要使用集合呢?数组与集合的区别?

  1. 数组的长度是固定的,但是集合的长度可以变换

  2. 数组可以存储基础数据类型,集合只能存储引用类型的数据

  3. 数组只有线性表一种数据结构,而集合有多种数据结构(线性表、Hash表、红黑树、二叉树等)

  4. 数组对数据的增删麻烦,而集合提供已经写好的Api(操作方便)

  5. !!

集合的体系结构

image

List接口及其实现类ArrayList、Vector、LinkedList

ArrayList的基本使用

ArrayList的增删改查方法(Vector、LinkedList同理)

  • 创建一个ArrayList集合对象
    // 创建一个ArrayList,并指定存储的数据类型为String,不指定则默认为Object类型
    ArrayList<String> arrayList = new ArrayList<>();
  • 增加元素
    // 添加一个元素,这个E其实就是在创建ArrayList时所指定的类型
    ArrayList.add(E e);
    
    // 在指定索引处添加一个元素
    ArrayList.add(int index, E e);
	
    // 添加一个集合
    ArrayList.addAll(Collection<? extends E> c);

    // 在指定索引处添加一个集合
    ArrayList.addAll(int index, Collection<? extends E> c)
  • 删除元素
    // 根据索引删除元素,索引从0开始
    ArrayList.remove(int index);

    // 根据元素内容删除元素
    ArrayList.remove("张三");
  • 修改一个元素
    // 为指定索引位置设置一个元素
    ArrayList.set(int index, E element);
  • 查找一个元素
    // 通过索引获得指定的元素
    ArrayList.get(int index);

List集合的三种遍历方式

fori遍历
public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();

        list1.add("张三");
        list1.add("李四");
        list1.add("王五");

        for (int i = 0; i < list1.size(); i++) {
            System.out.println(list1.get(i));
        }
    }
}
foreach遍历
public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();

        list1.add("张三");
        list1.add("李四");
        list1.add("王五");

        for (String str : list1) {
            System.out.println(str);
        }
    }
}
iterator遍历
public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();

        list1.add("张三");
        list1.add("李四");
        list1.add("王五");

        Iterator<String> iterator = list1.iterator();

        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}
ListIterator遍历
public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();

        list1.add("张三");
        list1.add("李四");
        list1.add("王五");

        ListIterator<String> listIterator = list1.listIterator();

        while (listIterator.hasNext()) {
            System.out.println(listIterator.next());
        }
    }
}

List集合特有的ListIterator迭代器

  • Iterator迭代器可以遍历List、Set集合,ListIterator只能用来遍历List集合

  • Iterator迭代器只能对集合向后遍历,ListIterator可以向前遍历也可以向后遍历

  • ListIterator是实现了Iterator接口,有很多其他的功能,例如可以增删改

    // 获取到一个ListInerator
    ListIterator<String> listIterator = new ArrayList<String>().listIterator();

    // 1. 是否有上一个
    listIterator.hasPrevious();
    // 2. 是否有下一个
    listIterator.hasNext();

    // 3. 获取上一个
    listIterator.previous();
    // 4. 获取下一个
    listIterator.next();

    // 5. 删除元素
    listIterator.remove();

    // 5. 添加元素
    listIterator.add("zhangsan");

    // 6. 修改元素
    listIterator.set("lisi");

    // 7. 获取上一次使用next所获取到数据的索引
    listIterator.previousIndex();
    // 8. 获取下一次使用next所获取到数据的索引
    listIterator.nextIndex();

List集合循环删除元素时需要注意的点

  • 使用fori删除元素时:在fori循环时删除元素会导致删除元素后数组的位置发生变化,导致数组出现获取或者删除混乱。只删除一次就break倒是可以。

  • 使用foreach删除元素时:如果删除超过1次以上,会直接抛出ConcurrentModificationException 并发修改异常

  • 使用迭代器Iterator或者ListIterator均不会出现问题,原因如下:

    • 迭代器对元素的遍历其实是有一个游标cursor,通过游标来获取元素。

    • 当获取到一个元素时,会有一个lastRet变量存储上一次获取到的值的索引。

    • 一旦元素发生删除时,数组的元素也会发生移动,而底层会将lastRet的值赋值给cursor,也就是最后获取的那个值的索引给了cursor

    • 因此就刚好指向了刚删除元素的索引位置,此时就指向了新移动过去的元素

posted @ 2022-07-29 13:25  CodeStars  阅读(235)  评论(0编辑  收藏  举报