模块16:多线程1
第一章.多线程基本了解
1.多线程_线程和进程
进程:在内存中执行的应用程序
线程:是进程中最小的执行单元
线程作用:负责当前进程中程序的运行.一个进程中至少有一个线程,一个进程还可以有多个线程,这样的应用程序就称之为多线程程序
简单理解:一个功能就需要一条线程取去执行
1.使用场景: 软件中的耗时操作 -> 拷贝大文件, 加载大量的资源
所有的聊天软件
所有的后台服务器
一个线程可以干一件事,我们就可以同时做多件事了,提高了CPU的利用率
2.并发和并行
并行:在同一个时刻,有多个执行在多个CPU上(同时)执行(好比是多个人做不同的事儿)
比如:多个厨师在炒多个菜
并发:在同一个时刻,有多个指令在单个CPU上(交替)执行
比如:一个厨师在炒多个菜
细节:
1.之前CPU是单核,但是在执行多个程序的时候好像是在同时执行,原因是CPU在多个线程之间做高速切换
2.现在咱们的CPU都是多核多线程的了,比如2核4线程,那么CPU可以同时运行4个线程,此时不同切换,但是如果多了,CPU就要切换了,所以现在CPU在执行程序的时候并发和并行都存在
3.CPU调度
1.分时调度:值的是让所有的线程轮流获取CPU使用权,并且平均分配每个线程占用CPU的时间片
2.抢占式调度:多个线程轮流抢占CPU使用权,哪个线程先抢到了,哪个线程先执行,一般都是优先级高的先抢到CPU使用权的几率大,我们java程序就是抢占式调度
4.主线程介绍
主线程:CPU和内存之间开辟的转门为main方法服务的线程
第二章.创建线程的方式(重点)
1.第一种方式_extends Thread
1.定义一个类,继承Thread
2.重写run方法,在run方法中设置线程任务(所谓的线程任务指的是此线程要干的具体的事儿,具体执行的代码)
3.创建自定义线程类的对象
4.调用Thread中的start方法,开启线程,jvm自动调用run方法
public class Test01 {
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread();
//调用start方法,开启线程,jvm自动调用run方法
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程..........执行了"+i);
}
}
}
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("MyThread...执行了"+i);
}
}
}
2.多线程在内存中的运行原理
开启一个线程,就会开启一个栈空间去运行对应的线程代码
注意:同一个线程对象不能连续调用多次start,如果想要再次调用start,那么咱们就new一个新的线程对象
3.Thread类中的方法
void start() -> 开启线程,jvm自动调用run方法
void run() -> 设置线程任务,这个run方法是Thread重写的接口Runnable中的run方法
String getName() -> 获取线程名字
void setName(String name) -> 给线程设置名字
static Thread currentThread() -> 获取正在执行的线程对象(此方法在哪个线程中使用,获取的就是哪个线程对象)
static void sleep(long millis)->线程睡眠,超时后自动醒来继续执行,传递的是毫秒值
问题:为啥在重写的run方法中有异常只能try,不能throws
原因:继承的Thread中的run方法没有抛异常,所以在子类中重写完run方法之后就不能抛,只能try
4.Thread中其他的方法
void setPriority(int newPriority) -> 设置线程优先级,优先级越高的线程,抢到CPU使用权的几率越大,但是不是每次都先抢到
int getPriority() -> 获取线程优先级
void setDaemon(boolean on) -> 设置为守护线程,当非守护线程执行完毕,守护线程就要结束,但是守护线程也不是立马结束,当非守护线程结束之后,系统会告诉守护线程人家结束了,你也结束吧,在告知的过程中,守护线程会执行,只不过执行到半路就结束了
static void yield() -> 礼让线程,让当前线程让出CPU使用权
void join() -> 插入线程或者叫做插队线程
4.1.线程优先级
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"执行了......"+i);
}
}
}
public class Test01 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("yy");
MyThread1 t2 = new MyThread1();
t2.setName("yyyy");
/*
获取两个线程的优先级
MIN_PRIORITY = 1 最小优先级 1
NORM_PRIORITY = 5 默认优先级 5
MAX_PRIORITY = 10 最大优先级 10
*/
//System.out.println(t1.getPriority());
//System.out.println(t2.getPriority());
//设置优先级
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
4.2.守护线程
public class Test01 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("yy");
MyThread2 t2 = new MyThread2();
t2.setName("yyyy");
//将t2设置成守护线程
t2.setDaemon(true);
t1.start();
t2.start();
}
}
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"执行了......"+i);
}
}
}
public class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"执行了..."+i);
}
}
}
4.3.礼让线程
场景说明:如果两个线程一起执行,可能会执行一会儿线程A,再执行一会线程B,或者可能线程A执行完毕了,线程B在执行
那么我们能不能让两个线程尽可能的平衡一点 -> 尽量让两个线程交替执行
注意:只是尽可能的平衡,不是绝对的你来我往,有可能线程A线程执行,然后礼让了,但是回头A又抢到CPU使用权了
public class Test01 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("yy");
MyThread1 t2 = new MyThread1();
t2.setName("yyyy");
t1.start();
t2.start();
}
}
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"执行了......"+i);
Thread.yield();
}
}
}
4.4.插入线程
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"执行了......"+i);
}
}
}
public class Test01 {
public static void main(String[] args) throws InterruptedException {
MyThread1 t1 = new MyThread1();
t1.setName("yy");
t1.start();
/*
表示把t1插入到当前线程之前,t1要插到main线程之前,所以当前线程就是main线程
*/
t1.join();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"执行了......"+i);
}
}
}
5.第二种方式_实现Runnable接口
1.创建类,实现Runnable接口
2.重写run方法,设置线程任务
3.利用Thread类的构造方法:Thread(Runnable target),创建Thread对象(线程对象),将自定义的类当参数传递到Thread构造中 -> 这一步是让我们自己定义的类成为一个真正的线程类对象
4.调用Thread中的start方法,开启线程,jvm自动调用run方法
public class Test01 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
/*
Thread(Runnable target)
*/
Thread t1 = new Thread(myRunnable);
//调用Thread中的start方法,开启线程
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"...执行了"+i);
}
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"...执行了"+i);
}
}
}
6.两种实现多线程的方式区别
1.继承Thread:继承只支持单继承,有继承的局限性
2.实现Runnable:没有继承的局限性, MyThread extends Fu implements Runnable
7.第三种方式_匿名内部类创建多线程
严格意义上来说,匿名内部类方式不属于创建多线程方式其中之一,因为匿名内部类形式建立在实现Runnable接口的基础上完成的
匿名内部类回顾:
1.new 接口/抽象类(){
重写方法
}.重写的方法();
2.接口名/类名 对象名 = new 接口/抽象类(){
重写方法
}
对象名.重写的方法();
public class Test02 {
public static void main(String[] args) {
/*
Thread(Runnable r)
Thread(Runnable target, String name) :name指的是给线程设置名字
*/
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"...执行了"+i);
}
}
},"yy").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"...执行了"+i);
}
}
},"yyyy").start();
}
}
第三章.线程安全
1.什么时候发生:当多个线程访问同一个资源时,导致了数据有问题
1.线程安全问题-->线程不安全的代码
public class MyTicket implements Runnable{
//定义100张票
int ticket = 100;
@Override
public void run() {
while(true){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}
}
}
public class Test01 {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
Thread t1 = new Thread(myTicket, "赵四");
Thread t2 = new Thread(myTicket, "刘能");
Thread t3 = new Thread(myTicket, "广坤");
t1.start();
t2.start();
t3.start();
}
}
原因:CPU在多个线程之间做高速切换导致的
2.解决线程安全问题的第一种方式(使用同步代码块)
1.格式:
synchronized(任意对象){
线程可能出现不安全的代码
}
2.任意对象:就是我们的锁对象
3.执行:
一个线程拿到锁之后,会进入到同步代码块中执行,在此期间,其他线程拿不到锁,就进不去同步代码块,需要在同步代码块外面等待排队,需要等着执行的线程执行完毕,出了同步代码块,相当于释放锁了,等待的线程才能抢到锁,才能进入到同步代码块中执行
public class MyTicket implements Runnable{
//定义100张票
int ticket = 100;
//任意new一个对象
Object obj = new Object();
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (obj){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}
}
}
}
3.解决线程安全问题的第二种方式:同步方法
3.1.普通同步方法_非静态
1.格式:
修饰符 synchronized 返回值类型 方法名(参数){
方法体
return 结果
}
2.默认锁:this
public class MyTicket implements Runnable{
//定义100张票
int ticket = 100;
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//method01();
method02();
}
}
/* public synchronized void method01(){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}*/
public void method02(){
synchronized(this){
System.out.println(this+"..........");
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}
}
}
3.2.静态同步方法
1.格式:
修饰符 static synchronized 返回值类型 方法名(参数){
方法体
return 结果
}
2.默认锁:class对象
public class MyTicket implements Runnable{
//定义100张票
static int ticket = 100;
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//method01();
method02();
}
}
/*public static synchronized void method01(){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}*/
public static void method02(){
synchronized(MyTicket.class){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
ticket--;
}
}
}
}
第四章.死锁(了解)
1.死锁介绍(锁嵌套就有可能产生死锁)
指的是两个或者两个以上的线程在执行的过程中由于竞争同步锁而产生的一种阻塞现象;如果没有外力的作用,他们将无法继续执行下去,这种情况称之为死锁
死锁举例:线程1正在持有锁1,但是线程1必须再拿到锁2,才能继续执行
而线程2正在持有锁2,但是线程2需要再拿到锁1,才能继续执行
此时两个线程处于互相等待的状态,就是死锁,在程序中的死锁将出现在同步代码块的嵌套中
2.死锁的分析
3.代码实现
public class LockA {
public static LockA lockA = new LockA();
}
public class LockB {
public static LockB lockB = new LockB();
}
public class DieLock implements Runnable{
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag){
synchronized (LockA.lockA){
System.out.println("if...lockA");
synchronized (LockB.lockB){
System.out.println("if...lockB");
}
}
}else{
synchronized (LockB.lockB){
System.out.println("else...lockB");
synchronized (LockA.lockA){
System.out.println("else...lockA");
}
}
}
}
}
public class Test01 {
public static void main(String[] args) {
DieLock dieLock1 = new DieLock(true);
DieLock dieLock2 = new DieLock(false);
new Thread(dieLock1).start();
new Thread(dieLock2).start();
}
}
只需要知道死锁出现的原因即可(锁嵌套),以后尽量避免锁嵌套
第五章.线程状态
1.线程状态介绍
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析。
| 线程状态 | 导致状态发生条件 |
|---|---|
| NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
| Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
| Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
| Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
| Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
| Terminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。或者调用过时方法stop() |
2.线程状态图
模块17:多线程2
第一章.等待唤醒机制
1.等待唤醒案例分析(线程之间的通信)
要求:一个线程生产,一个线程消费,不能连续生产,不能连续消费 -> 等待唤醒机制(生产者,消费者)(线程之间的通信)
| 方法 | 说明 |
|---|---|
| void wait() | 线程等待,等待的过程中线程会释放锁,需要被其他线程调用notify方法将其唤醒,重新抢锁执行 |
| void notify() | 线程唤醒,一次唤醒一个等待线程;如果有多条线程等待,则随机唤醒一条等待线程 |
| void notifyAll() | 唤醒所有等待线程 |
wait和notify方法需要锁对象调用,所以需要用到同步代码块中,而且必须是同一个锁对象
2.等待唤醒案例实现
/*
count和flag可以定义成包装类
但是要记得给count和flag手动赋值
不然对于本案例来说,容易出现空指针异常
*/
public class BaoZiPu {
//代表包子的count
private int count;
//代表是否有包子的flag
private boolean flag;
public BaoZiPu() {
}
public BaoZiPu(int count, boolean flag) {
this.count = count;
this.flag = flag;
}
/*
getCount 改造成消费包子方法
直接输出count
*/
public void getCount() {
System.out.println("消费了..............第"+count+"个包子");
}
/*
setCount 改造成生产包子
count++
*/
public void setCount() {
count++;
System.out.println("生产了...第"+count+"个包子");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (baoZiPu){
//1.判断flag是否为true,如果是true,证明有包子,生产线程等待
if (baoZiPu.isFlag()==true){
try {
baoZiPu.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//2.如果flag为false,证明没有包子,开始生产
baoZiPu.setCount();
//3.改变flag状态,为true,证明生产完了,有包子了
baoZiPu.setFlag(true);
//4.唤醒消费线程
baoZiPu.notify();
}
}
}
}
public class Consumer implements Runnable{
private BaoZiPu baoZiPu;
public Consumer(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (baoZiPu){
//1.判断flag是否为false,如果是false,证明没有包子,消费线程等待
if (baoZiPu.isFlag()==false){
try {
baoZiPu.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//2.如果flag为true,证明有包子,开始消费
baoZiPu.getCount();
//3.改变flag状态,为false,证明消费完了,没 有包子了
baoZiPu.setFlag(false);
//4.唤醒生产线程
baoZiPu.notify();
}
}
}
}
public class Test01 {
public static void main(String[] args) {
BaoZiPu baoZiPu = new BaoZiPu();
Product product = new Product(baoZiPu);
Consumer consumer = new Consumer(baoZiPu);
Thread t1 = new Thread(product);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
3.用同步方法改造等待唤醒案例
/*
count和flag可以定义成包装类
但是要记得给count和flag手动赋值
不然对于本案例来说,容易出现空指针异常
*/
public class BaoZiPu {
//代表包子的count
private int count;
//代表是否有包子的flag
private boolean flag;
public BaoZiPu() {
}
public BaoZiPu(int count, boolean flag) {
this.count = count;
this.flag = flag;
}
/*
getCount 改造成消费包子方法
直接输出count
*/
public synchronized void getCount() {
//1.判断flag是否为false,如果是false,证明没有包子,消费线程等待
if (this.flag == false) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//2.如果flag为true,证明有包子,开始消费
System.out.println("消费了..............第" + count + "个包子");
//3.改变flag状态,为false,证明消费完了,没 有包子了
this.flag = false;
//4.唤醒生产线程
this.notify();
}
/*
setCount 改造成生产包子
count++
*/
public synchronized void setCount() {
//1.判断flag是否为true,如果是true,证明有包子,生产线程等待
if (this.flag == true) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//2.如果flag为false,证明没有包子,开始生产
count++;
System.out.println("生产了...第" + count + "个包子");
//3.改变flag状态,为true,证明生产完了,有包子了
this.flag = true;
//4.唤醒消费线程
this.notify();
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
baoZiPu.setCount();
}
}
}
public class Consumer implements Runnable{
private BaoZiPu baoZiPu;
public Consumer(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
baoZiPu.getCount();
}
}
}
public class Test01 {
public static void main(String[] args) {
BaoZiPu baoZiPu = new BaoZiPu();
Product product = new Product(baoZiPu);
Consumer consumer = new Consumer(baoZiPu);
Thread t1 = new Thread(product);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
第二章.多等待多唤醒
1.解决多生产多消费问题(if改为while,将notify改为notifyAll)
public class Test01 {
public static void main(String[] args) {
BaoZiPu baoZiPu = new BaoZiPu();
Product product = new Product(baoZiPu);
Consumer consumer = new Consumer(baoZiPu);
new Thread(product).start();
new Thread(product).start();
new Thread(product).start();
new Thread(consumer).start();
new Thread(consumer).start();
new Thread(consumer).start();
}
}
/*
count和flag可以定义成包装类
但是要记得给count和flag手动赋值
不然对于本案例来说,容易出现空指针异常
*/
public class BaoZiPu {
//代表包子的count
private int count;
//代表是否有包子的flag
private boolean flag;
public BaoZiPu() {
}
public BaoZiPu(int count, boolean flag) {
this.count = count;
this.flag = flag;
}
/*
getCount 改造成消费包子方法
直接输出count
*/
public synchronized void getCount() {
//1.判断flag是否为false,如果是false,证明没有包子,消费线程等待
while (this.flag == false) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//2.如果flag为true,证明有包子,开始消费
System.out.println("消费了..............第" + count + "个包子");
//3.改变flag状态,为false,证明消费完了,没 有包子了
this.flag = false;
//4.唤醒所有等待线程
this.notifyAll();
}
/*
setCount 改造成生产包子
count++
*/
public synchronized void setCount() {
//1.判断flag是否为true,如果是true,证明有包子,生产线程等待
while (this.flag == true) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//2.如果flag为false,证明没有包子,开始生产
count++;
System.out.println("生产了...第" + count + "个包子");
//3.改变flag状态,为true,证明生产完了,有包子了
this.flag = true;
//4.唤醒所有等待线程
this.notifyAll();
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
baoZiPu.setCount();
}
}
}
public class Consumer implements Runnable{
private BaoZiPu baoZiPu;
public Consumer(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
baoZiPu.getCount();
}
}
}
第三章.Lock锁
1.Lock对象的介绍和基本使用
1.概述:Lock是一个接口
2.实现类:ReentrantLock
3.方法:
lock() 获取锁
unlock() 释放锁
public class MyTicket implements Runnable {
//定义100张票
int ticket = 100;
//创建Lock对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
Thread.sleep(100L);
//获取锁
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");
ticket--;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放锁
lock.unlock();
}
}
}
}
public class Test01 {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
Thread t1 = new Thread(myTicket, "赵四");
Thread t2 = new Thread(myTicket, "刘能");
Thread t3 = new Thread(myTicket, "广坤");
t1.start();
t2.start();
t3.start();
}
}
synchronized:不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放锁对象
Lock:是通过两个方法控制需要被同步的代码,更灵活
第四章.Callable接口_实现多线程方式三
1.概述:Callable<V>是一个接口,类似于Runnable
2.方法:
V call() -> 设置线程任务的,类似于run方法
3.call方法和run方法的区别:
a.相同点:都是设置线程任务的
b.不同点:
call方法有返回值,而且有异常可以throws
run方法没有返回值,而且有异常不可以throws
4.<V>
a.<V>叫做泛型
b.泛型:用于指定我们操作什么类型的数据,<>中只能写引用数据类型,如果泛型不写,默认是Object类型数据
c.实现Callable接口时,指定泛型是什么类型的,重写的call方法返回值就是什么类型的
5.获取call方法返回值: FutureTask<V>
a. FutureTask<V> 实现了一个接口: Future <V>
b. FutureTask<V>中有一个方法:
V get() -> 获取call方法的返回值
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "yy";
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
/*
FutureTask(Callable<V> callable)
*/
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//创建Thread对象-> Thread(Runnable target)
Thread t1 = new Thread(futureTask);
t1.start();
//调用get方法获取call方法返回值
System.out.println(futureTask.get());
}
}
第五章.线程池_实现多线程方式四
1.问题:之前来一个线程任务,就需要创建一个线程对象去执行,用完还要销毁线程对象,
如果线程任务多了,就需要频繁创建线程对象和销毁线程对象,这样会耗费内存资源,
所以我们就想线程对象能不能循环利用,用的时候直接拿线程对象,用完还回去
1.如何创建线程池对象:用具类-> Executors
2.获取线程池对象:Executors中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads)
a.参数:指定线程池中最多创建的线程对象条数
b.返回值ExecutorService 是线程池,用来管理线程对象
3.执行线程任务: ExecutorService中的方法
Future<?> submit(Runnable task) 提交一个Runnable任务用于执行
Future<T> submit(Callable<T> task) 提交一个Callable任务用于执行
4.submit方法的返回值:Future接口
用于接收run方法或者call方法返回值的,但是run方法没有返回值,所以可以不用Future接收,执行call方法需要用Future接收
Future中有一个方法:V get() 用于获取call方法返回值
5. ExecutorService中的方法:
void shutdown() 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"...执行了");
}
}
public class Test01 {
public static void main(String[] args) {
//创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(new MyRunnable());
es.submit(new MyRunnable());
es.submit(new MyRunnable());
//es.shutdown();//关闭线程池对象
}
}
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1;
}
}
public class Test02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Integer> future = es.submit(new MyCallable());
System.out.println(future.get());
}
}
第六章.定时器_Timer
1.概述:定时器
2.构造:
Timer()
3.方法:
void schedule(TimerTask task, Date firstTime, long period)
task:抽象类,是Runnable的实现类
firstTime:从什么时间开始执行
period: 每隔多长时间执行一次,设置的是毫秒值
public class Demo01Timer {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("yyyy~~~");
}
},new Date(),2000L);
}
}
模块18:集合1-List
第一章.集合框架(单列集合)
1.之前我们学了保存数据的有:变量,数组,但是数组定长,所以如果添加一个数据或者删除一个数据,数组并不好使,需要创建新数组,所以接下来我们学一个长度可变的容器,集合
2.集合的特点
a.只能存储引用数据类型的数据
b.长度可变
c.集合中有大量的方法,方便我们操作
3.分类:
a.单列集合:一个元素就一个组成部分:
list.add("张三")
b.双列集合:一个元素有两部分构成: key 和 value
map.put("张三","李四") -> key,value叫做键值对
第二章.Collection接口
1.概述:单列集合的顶级接口
2.使用:
a.创建:
Collection<E> 对象名 = new 实现类对象<E>()
b.<E>:泛型,决定了集合中能存储什么类型的数据,可以统一元素类型
泛型中只能写引用数据类型,如果不写,默认Object类型,此时什么类型的数据都可以存储了
<int> 不行
<Integer> 行
<Person> 行
c.泛型细节:
我们等号前面的泛型必须写,等号后面的泛型可以不写,jvm会根据前面的泛型推导出后面的泛型是啥
3.常用方法:
boolean add(E e) : 将给定的元素添加到当前集合中(我们一般调add时,不用boolean接收,因为add一定会成功)
boolean addAll(Collection<? extends E> c) :将另一个集合元素添加到当前集合中 (集合合并)
void clear():清除集合中所有的元素
boolean contains(Object o) :判断当前集合中是否包含指定的元素
boolean isEmpty() : 判断当前集合中是否有元素->判断集合是否为空
boolean remove(Object o):将指定的元素从集合中删除
int size() :返回集合中的元素个数。
Object[] toArray(): 把集合中的元素,存储到数组中
public class Demo01Collection {
public static void main(String[] args) {
Collection<String> collection = new ArrayList<>();
//boolean add(E e) : 将给定的元素添加到当前集合中(我们一般调add时,不用boolean接收,因为add一定会成功)
collection.add("萧炎");
collection.add("萧薰儿");
collection.add("彩鳞");
collection.add("小医仙");
collection.add("云韵");
collection.add("涛哥");
System.out.println(collection);
//boolean addAll(Collection<? extends E> c) :将另一个集合元素添加到当前集合中 (集合合并)
Collection<String> collection1 = new ArrayList<>();
collection1.add("张无忌");
collection1.add("小昭");
collection1.add("赵敏");
collection1.add("周芷若");
collection1.addAll(collection);
System.out.println(collection1);
//void clear():清除集合中所有的元素
collection1.clear();
System.out.println(collection1);
//boolean contains(Object o) :判断当前集合中是否包含指定的元素
boolean result01 = collection.contains("涛哥");
System.out.println("result01 = " + result01);
//boolean isEmpty() : 判断当前集合中是否有元素->判断集合是否为空
System.out.println(collection1.isEmpty());
//boolean remove(Object o):将指定的元素从集合中删除
collection.remove("涛哥");
System.out.println(collection);
//int size() :返回集合中的元素个数。
System.out.println(collection.size());
//Object[] toArray(): 把集合中的元素,存储到数组中
Object[] arr = collection.toArray();
System.out.println(Arrays.toString(arr));
}
}
第三章.迭代器
1.迭代器基本使用
1.概述:Iterator接口
2.主要作用:遍历集合
3.获取:Collection中的方法:
Iterator<E> iterator()
4.方法:
boolean hasNext() -> 判断集合中有没有下一个元素
E next() ->获取下一个元素
public class Demo01Iterator {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("楚雨荨");
list.add("慕容云海");
list.add("端木磊");
list.add("上官瑞谦");
list.add("叶烁");
//获取迭代器对象
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String element = iterator.next();
System.out.println(element);
}
}
}
注意:next方法在获取的时候不要连续使用多次
public class Demo02Iterator {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("楚雨荨");
list.add("慕容云海");
list.add("端木磊");
list.add("上官瑞谦");
list.add("叶烁");
//获取迭代器对象
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String element = iterator.next();
System.out.println(element);
//String element2 = iterator.next();
//System.out.println(element2);
}
}
}
NoSuchElementException:没有可操作的元素异常
2.迭代器迭代过程
int cursor; //下一个元素索引位置
int lastRet = -1;//上一个元素索引位置
3.迭代器底层原理
1.获取Iterator的时候怎么获取的:
Iterator iterator = list.iterator()
我们知道Iterator是一个接口,等号右边一定是它的实现类对象
问题:Iterator接收的到底是哪个实现类对象呢? -> ArrayList中的内部类Itr对象
注意:只有ArrayList使用迭代器的时候Iterator接口才会指向Itr,其他的集合使用迭代器Iterator就指向的不是Itr了
HashSet<String> set = new HashSet<>();
Iterator<String> iterator1 = set.iterator();
4.并发修改异常
需求:定义一个集合,存储 唐僧,孙悟空,猪八戒,沙僧,遍历集合,如果遍历到猪八戒,往集合中添加一个白龙马
public class Demo03Iterator {
public static void main(String[] args) {
//需求:定义一个集合,存储 唐僧,孙悟空,猪八戒,沙僧,遍历集合,如果遍历到猪八戒,往集合中添加一个白龙马
ArrayList<String> list = new ArrayList<>();
list.add("唐僧");
list.add("孙悟空");
list.add("猪八戒");
list.add("沙僧");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String element = iterator.next();
if ("猪八戒".equals(element)){
list.add("白龙马");
}
}
System.out.println(list);
}
}
String element = iterator.next();
=========================================
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
/*
modCount: 实际操作次数
expectedModCount:预期操作次数
*/
int expectedModCount = modCount;
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
结论:当预期操作次数和实际操作次数不相等了,会出现"并发修改异常"
我们干了什么事儿,让实际操作次数和预期操作次数不相等了
list.add("白龙马")
====================================
public boolean add(E e) {
modCount++;//实际操作次数+1
}
====================================
最终结论:我们调用了add方法,而add方法底层只给modCount++,但是再次调用next方法的时候,并没有给修改后的modCount重新赋值给expectedModCount,导致next方法底层的判断判断出实际操作次数和预期操作次数不相等了,所以抛出了"并发修改异常"
ArrayList中的方法:ListIterator listIterator()
public class Demo03Iterator {
public static void main(String[] args) {
//需求:定义一个集合,存储 唐僧,孙悟空,猪八戒,沙僧,遍历集合,如果遍历到猪八戒,往集合中添加一个白龙马
ArrayList<String> list = new ArrayList<>();
list.add("唐僧");
list.add("孙悟空");
list.add("猪八戒");
list.add("沙僧");
//Iterator<String> iterator = list.iterator();
ListIterator<String> listIterator = list.listIterator();
while(listIterator.hasNext()){
String element = listIterator.next();
if ("猪八戒".equals(element)){
listIterator.add("白龙马");
}
}
System.out.println(list);
}
}
使用迭代器迭代集合的过程中,不要随意修改集合长度,容易出现并发修改异常
第四章.数据结构
数据结构是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素集合。它包含三方面的内容,逻辑关系、存储关系及操作。
0.为什么需要数据结构
随着应用程序变得越来越复杂和数据越来越丰富,几百万、几十亿甚至几百亿的数据就会出现,而对这么大对数据进行搜索、插入或者排序等的操作就越来越慢,数据结构就是用来解决这些问题的。
数据的逻辑结构指反映数据元素之间的逻辑关系,而与他们在计算机中的存储位置无关:
- 集合(数学中集合的概念):数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系;
- 线性结构:数据结构中的元素存在一对一的相互关系;
- 树形结构:数据结构中的元素存在一对多的相互关系;
- 图形结构:数据结构中的元素存在多对多的相互关系
1.栈
1.特点:
先进后出
2.好比:手枪压子弹
2.队列
1.特点:先进先出
2.好比:过安检
3.数组
1.特点:查询快,增删慢
2.查询快:因为有索引,我们可以直接通过索引操作元素
增删慢:因为数组定长
a.添加元素:创建新数组,将老数组中的元素复制到新数组中去,在最后添加新元素;要是从中间插入就更麻烦了
插入完新元素,后面的元素都要往后移动
b.删除元素:创建新数组,将老数组中的元素复制到新数组中去,被删除的元素就不复制了;如果要是从之间删除
被删除的元素后面的元素都要往前移动
4.链表
1.在集合中涉及到了两种链表
2.单向链表
a.节点:一个节点分为两部分
第一部分:数据域(存数据)
第二部分:指针域(保存下一个节点地址)
b.特点:前面节点记录后面节点的地址,但是后面节点地址不记录前面节点地址
3.双向链表:
a.节点:一个节点分为三部分
第一部分:指针域(保存上一个节点地址)
第二部分:数据域(保存的数据)
第三部分:指针域(保存下一个节点地址)
b.特点:
前面节点记录后面节点地址,后面节点也记录前面节点地址
4.链表结构特点:查询慢,增删快
4.1单向链表
a.节点:一个节点分为两部分
第一部分:数据域(存数据)
第二部分:指针域(保存下一个节点地址)
b.特点:前面节点记录后面节点的地址,但是后面节点地址不记录前面节点地址
4.2双向链表
a.节点:一个节点分为三部分
第一部分:指针域(保存上一个节点地址)
第二部分:数据域(保存的数据)
第三部分:指针域(保存下一个节点地址)
b.特点:
前面节点记录后面节点地址,后面节点也记录前面节点地址
第五章.List接口
1.概述:是Collection接口的子接口
2.常见的实现类:
ArrayList LinkedList Vector
第六章.List集合下的实现类
1.ArrayList集合
1.概述:ArrayList是List接口的实现类
2.特点:
a.元素有序-> 按照什么顺序存的,就按照什么顺序取
b.元素可重复
c.有索引-> 可以利用索引去操作元素
d.线程不安全
3.数据结构:数组
4.常用方法:
boolean add(E e) -> 将元素添加到集合中->尾部(add方法一定能添加成功的,所以我们不用boolean接收返回值)
void add(int index, E element) ->在指定索引位置上添加元素
boolean remove(Object o) ->删除指定的元素,删除成功为true,失败为false
E remove(int index) -> 删除指定索引位置上的元素,返回的是被删除的那个元素
E set(int index, E element) -> 将指定索引位置上的元素,修改成后面的element元素
E get(int index) -> 根据索引获取元素
int size() -> 获取集合元素个数
1.2.ArrayList底层源码分析
1.ArrayList构造方法:
a.ArrayList() 构造一个初始容量为十的空列表
b.ArrayList(int initialCapacity) 构造具有指定初始容量的空列表
2.ArrayList源码总结:
a.不是一new底层就会创建初始容量为10的空列表,而是第一次add的时候才会创建初始化容量为10的空列表
b.ArrayList底层是数组,那么为啥还说集合长度可变呢?
ArrayList底层会自动扩容-> Arrays.copyOf
c.扩容多少倍?
1.5倍
ArrayList list = new ArrayList() -> 现在我们想用都是new
但是将来开发不会想使用就new集合,都是调用一个方法,查询出很多数据来,此方法返回一个集合,自动将查询出来的数据放到集合中,我们想在页面上展示数据,遍历集合
而且将来调用方法,返回的集合类型,一般都是接口类型
List<泛型> list = 对象.查询方法()
第七章.LinkedList集合
1.LinkedList集合
1.概述:LinkedList是List接口的实现类
2.特点:
a.元素有序
b.元素可重复
c.有索引 -> 这里说的有索引仅仅指的是有操作索引的方法,不代表本质上具有索引
d.线程不安全
3.数据结构:双向链表
4.方法:有大量直接操作首尾元素的方法
- public void addFirst(E e):将指定元素插入此列表的开头。
- public void addLast(E e):将指定元素添加到此列表的结尾。
- public E getFirst():返回此列表的第一个元素。
- public E getLast():返回此列表的最后一个元素。
- public E removeFirst():移除并返回此列表的第一个元素。
- public E removeLast():移除并返回此列表的最后一个元素。
- public E pop():从此列表所表示的堆栈处弹出一个元素。
- public void push(E e):将元素推入此列表所表示的堆栈。
- public boolean isEmpty():如果列表没有元素,则返回true。
1.1 LinkedList底层成员解释说明
1.LinkedList底层成员
transient int size = 0; 元素个数
transient Node<E> first; 第一个节点对象
transient Node<E> last; 最后一个节点对象
2.Node代表的是节点对象
private static class Node<E> {
E item;//节点上的元素
Node<E> next;//记录着下一个节点地址
Node<E> prev;//记录着上一个节点地址
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
1.2 LinkedList中add方法源码分析
LinkedList<String> list = new LinkedList<>();
list.add("a");
list.add("b");
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
1.3.LinkedList中get方法源码分析
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
index < (size >> 1)采用二分思想,先将index与长度size的一半比较,如果index<size/2,就只从位置0往后遍历到位置index处,而如果index>size/2,就只从位置size往前遍历到位置index处。这样可以减少一部分不必要的遍历
第八章.增强for
1.基本使用
1.作用:
遍历集合或者数组
2.格式:
for(元素类型 变量名:要遍历的集合名或者数组名){
变量名就是代表的每一个元素
}
3.快捷键:集合名或者数组名.for
int[] arr = {1,2,3,4,5};
for (int i : arr) {
System.out.println(i);
}
2.增强for原理
1.增强for遍历集合时,底层实现原理为迭代器
2.增强for遍历数组时,底层实现原理为普通for
所以不管是用迭代器还是使用增强for,在遍历集合的过程中都不要随意修改集合长度,否则会出现并发修改异常
模块19:集合2-Set
第一章.Collections集合工具类
1.概述:集合工具类
2.特点:
a.构造私有
b.方法都是静态的
3.使用:类名直接调用
4.方法:
static <T> boolean addAll(Collection<? super T> c, T... elements)->批量添加元素
static void shuffle(List<?> list) ->将集合中的元素顺序打乱
static <T> void sort(List<T> list) ->将集合中的元素按照默认规则排序
static <T> void sort(List<T> list, Comparator<? super T> c)->将集合中的元素按照指定规则排序
public class Demo01Collections {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
//static <T> boolean addAll(Collection<? super T> c, T... elements)->批量添加元素
Collections.addAll(list,"张三","李四","王五","赵六","田七","朱八");
System.out.println(list);
//static void shuffle(List<?> list) ->将集合中的元素顺序打乱
Collections.shuffle(list);
System.out.println(list);
//static <T> void sort(List<T> list) ->将集合中的元素按照默认规则排序-> ASCII码表
ArrayList<String> list1 = new ArrayList<>();
list1.add("c.举头望明月");
list1.add("a.床前明月光");
list1.add("d.低头思故乡");
list1.add("b.疑是地上霜");
Collections.sort(list1);
System.out.println(list1);
}
}
1.方法:static <T> void sort(List<T> list, Comparator<? super T> c)->将集合中的元素按照指定规则排序
2.Comparator比较器
a.方法:
int compare(T o1,T o2)
o1-o2 -> 升序
o2-o1 -> 降序
public class Person {
private String name;
private Integer age;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
public class Demo02Collections {
public static void main(String[] args) {
ArrayList<Person> list = new ArrayList<>();
list.add(new Person("张三",18));
list.add(new Person("李四",16));
list.add(new Person("王五",20));
Collections.sort(list, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()-o2.getAge();
}
});
System.out.println(list);
}
}
1.接口:Comparable接口
2.方法: int compareTo(T o) -> this-o (升序) o-this(降序)
public class Student implements Comparable<Student>{
private String name;
private Integer score;
public Student() {
}
public Student(String name, Integer score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + ''' +
", score=" + score +
'}';
}
@Override
public int compareTo(Student o) {
return this.getScore()-o.getScore();
}
}
public class Demo03Collections {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("张三",100));
list.add(new Student("李四",150));
list.add(new Student("王五",80));
Collections.sort(list);
System.out.println(list);
}
}
Arrays中的静态方法:
static <T> List<T> asList(T...a) -> 直接指定元素,转存到list集合中
public class Demo04Collections {
public static void main(String[] args) {
List<String> list = Arrays.asList("张三", "李四", "王五");
System.out.println(list);
}
}
第二章.泛型
1.泛型:<>
2.作用:
统一数据类型,防止将来的数据转换异常
3.注意:
a.泛型中的类型必须是引用类型
b.如果泛型不写,默认类型为Object
1.为什么要使用泛型
1.从使用层面上来说:
统一数据类型,防止将来的数据类型转换异常
2.从定义层面上来看:
定义带泛型的类,方法等,将来使用的时候给泛型确定什么类型,泛型就会变成什么类型,凡是涉及到泛型的都会变成确定的类型,代码更灵活
2.泛型的定义
2.1含有泛型的类
1.定义:
public class 类名<E>{
}
2.什么时候确定类型
new对象的时候确定类型
public class MyArrayList <E>{
//定义一个数组,充当ArrayList底层的数组,长度直接规定为10
Object[] obj = new Object[10];
//定义size,代表集合元素个数
int size;
/**
* 定义一个add方法,参数类型需要和泛型类型保持一致
*
* 数据类型为E 变量名随便取
*/
public boolean add(E e){
obj[size] = e;
size++;
return true;
}
/**
* 定义一个get方法,根据索引获取元素
*/
public E get(int index){
return (E) obj[index];
}
@Override
public String toString() {
return Arrays.toString(obj);
}
}
public class Demo02Genericity {
public static void main(String[] args) {
MyArrayList<String> list1 = new MyArrayList<>();
list1.add("aaa");
list1.add("bbb");
System.out.println(list1);//直接输出对象名,默认调用toString
System.out.println("===========");
MyArrayList<Integer> list2 = new MyArrayList<>();
list2.add(1);
list2.add(2);
Integer element = list2.get(0);
System.out.println(element);
System.out.println(list2);
}
}
2.2含有泛型的方法
1.格式:
修饰符 <E> 返回值类型 方法名(E e)
2.什么时候确定类型
调用的时候确定类型
public class ListUtils {
//定义一个静态方法addAll,添加多个集合的元素
public static <E> void addAll(ArrayList<E> list,E...e){
for (E element : e) {
list.add(element);
}
}
}
public class Demo03Genericity {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
ListUtils.addAll(list1,"a","b","c");
System.out.println(list1);
System.out.println("================");
ArrayList<Integer> list2 = new ArrayList<>();
ListUtils.addAll(list2,1,2,3,4,5);
System.out.println(list2);
}
}
2.3含有泛型的接口
1.格式:
public interface 接口名<E>{
}
2.什么时候确定类型:
a.在实现类的时候还没有确定类型,只能在new实现类的时候确定类型了 ->比如 ArrayList
b.在实现类的时候直接确定类型了 -> 比如Scanner
public interface MyList <E>{
public boolean add(E e);
}
ublic class MyArrayList1<E> implements MyList<E>{
//定义一个数组,充当ArrayList底层的数组,长度直接规定为10
Object[] obj = new Object[10];
//定义size,代表集合元素个数
int size;
/**
* 定义一个add方法,参数类型需要和泛型类型保持一致
*
* 数据类型为E 变量名随便取
*/
public boolean add(E e){
obj[size] = e;
size++;
return true;
}
/**
* 定义一个get方法,根据索引获取元素
*/
public E get(int index){
return (E) obj[index];
}
@Override
public String toString() {
return Arrays.toString(obj);
}
}
public class Demo04Genericity {
public static void main(String[] args) {
MyArrayList1<String> list1 = new MyArrayList1<>();
list1.add("张三");
list1.add("李四");
System.out.println(list1.get(0));
}
}
3.泛型的高级使用
3.1 泛型通配符 ?
public class Demo01Genericity {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("张三");
list1.add("李四");
ArrayList<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
method(list1);
method(list2);
}
public static void method(ArrayList<?> list){
for (Object o : list) {
System.out.println(o);
}
}
}
3.2 泛型的上限下限
1.作用:可以规定泛型的范围
2.上限:
a.格式:<? extends 类型>
b.含义:?只能接收extends后面的本类类型以及子类类型
3.下限:
a.格式:<? super 类型>
b.含义:?只能接收super后面的本类类型以及父类类型
应用场景:
1.如果我们在定义类,方法,接口的时候,如果类型不确定,我们可以考虑定义含有泛型的类,方法,接口
2.如果类型不确定,但是能知道以后只能传递某个类的继承体系中的子类或者父类,就可以使用泛型的通配符
第三章.红黑树(了解)
集合加入红黑树的目的:提高查询效率
HashSet集合:
数据结构:哈希表
jdk8之前:哈希表 = 数组+链表
jdk8之后:哈希表 = 数组+链表+红黑树 ->为的是查询效率
1. 每一个节点或是红色的,或者是黑色的
2. 根节点必须是黑色
3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况)
5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
第四章.Set集合
1.Set集合介绍
表示不允许重复元素的无序集合。它继承自 Collection 接口,并扩展了唯一性约束,常用于需要去重或快速查找的场景。
- 唯一性:集合中不能包含重复元素(通过
equals()和hashCode()方法判断)。 - 无序性:不保证元素的存储顺序(但某些实现类可能有序,如
LinkedHashSet)。 - 不允许
null值:部分实现类允许单个null(如HashSet),但依赖具体实现。
2.HashSet集合的介绍和使用
1.概述:HashSet是Set接口的实现类
2.特点:
a.元素唯一
b.元素无序
c.无索引
d.线程不安全
3.数据结构:哈希表
a.jdk8之前:哈希表 = 数组+链表
b.jdk8之后:哈希表 = 数组+链表+红黑树
加入红黑树目的:查询快
4.方法:和Collection一样
5.遍历:
a.增强for
b.迭代器
3.LinkedHashSet的介绍以及使用
1.概述:LinkedHashSet extends HashSet
2.特点:
a.元素唯一
b.元素有序
c.无索引
d.线程不安全
3.数据结构:
哈希表+双向链表
4.使用:和HashSet一样
4.哈希值
1.概述:是由计算机算出来的一个十进制数,可以看做是对象的地址值
2.获取对象的哈希值,使用的是Object中的方法
public native int hashCode()
3.注意:如果重写了hashCode方法,那计算的就是对象内容的哈希值了
4.总结:
a.哈希值不一样,内容肯定不一样
b.哈希值一样,内容也有可能不一样
如果不重写hashCode方法,默认计算对象的哈希值
如果重写了hashCode方法,计算的是对象内容的哈希值
5.HashSet的存储去重复的过程
1.先计算元素的哈希值(重写hashCode方法),再比较内容(重写equals方法)
2.先比较哈希值,如果哈希值不一样,存
3.如果哈希值一样,再比较内容
a.如果哈希值一样,内容不一样,存
b.如果哈希值一样,内容也一样,去重复
6.HashSet存储自定义类型如何去重复
public class Person {
private String name;
private Integer age;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name) && Objects.equals(age, person.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
public class Test03 {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
set.add(new Person("涛哥",16));
set.add(new Person("金莲",24));
set.add(new Person("涛哥",16));
System.out.println(set);
}
}
总结:
1.如果HashSet存储自定义类型,如何去重复呢?重写hashCode和equals方法,让HashSet比较属性的哈希值以及属性的内容
2.如果不重写hashCode和equals方法,默认调用的是Object中的,不同的对象,肯定哈希值不一样,equals比较对象的地址值也不一样,所以此时即使对象的属性值一样,也不能去重复
模块20:集合3-Map
第一章.Map集合
1.Map的介绍
1.概述:是双列集合的顶级接口
2.元素特点:
元素都是由key(键),value(值)组成 -> 键值对
2.HashMap的介绍和使用
1.概述:HashMap是Map的实现类
2.特点:
a.key唯一,value可重复 -> 如果key重复了,会发生value覆盖
b.无序
c.无索引
d.线程不安全
e.可以存null键null值
3.数据结构:
哈希表
4.方法:
V put(K key, V value) -> 添加元素,返回的是
V remove(Object key) ->根据key删除键值对,返回的是被删除的value
V get(Object key) -> 根据key获取value
boolean containsKey(Object key) -> 判断集合中是否包含指定的key
Collection<V> values() -> 获取集合中所有的value,转存到Collection集合中
Set<K> keySet()->将Map中的key获取出来,转存到Set集合中
Set<Map.Entry<K,V>> entrySet()->获取Map集合中的键值对,转存到Set集合中
public class Demo01HashMap {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
//V put(K key, V value) -> 添加元素,返回的是被覆盖的value
String value1 = map.put("猪八", "嫦娥");
System.out.println(value1);
String value2 = map.put("猪八", "高翠兰");
System.out.println(value2);
System.out.println(map);
map.put("后裔","嫦娥");
map.put("二郎神","嫦娥");
map.put("唐僧","女儿国国王");
map.put(null,null);
System.out.println(map);
//V remove(Object key) ->根据key删除键值对,返回的是被删除的value
String value3 = map.remove("涛哥");
System.out.println(value3);
System.out.println(map);
//V get(Object key) -> 根据key获取value
System.out.println(map.get("唐僧"));
//boolean containsKey(Object key) -> 判断集合中是否包含指定的key
System.out.println(map.containsKey("二郎神"));
//Collection<V> values() -> 获取集合中所有的value,转存到Collection集合中
Collection<String> collection = map.values();
System.out.println(collection);
}
}
1.概述:LinkedHashMap extends HashMap
2.特点:
a.key唯一,value可重复 -> 如果key重复了,会发生value覆盖
b.有序
c.无索引
d.线程不安全
e.可以存null键null值
3.数据结构:
哈希表+双向链表
4.使用:和HashMap一样
public class Demo02LinkedHashMap {
public static void main(String[] args) {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("八戒","嫦娥");
map.put("唐僧","女儿国国王");
System.out.println(map);
}
}
3.HashMap的两种遍历方式
3.1.方式1:获取key,根据key再获取value
Set<K> keySet()->将Map中的key获取出来,转存到Set集合中
public class Demo03HashMap {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("猪八", "嫦娥");
map.put("猪八", "高翠兰");
map.put("后裔","嫦娥");
map.put("二郎神","嫦娥");
map.put("唐僧","女儿国国王");
Set<String> set = map.keySet();//获取所有的key,保存到set集合中
for (String key : set) {
//根据key获取value
System.out.println(key+".."+map.get(key));
}
}
}
3.2.方式2:同时获取key和value
Set<Map.Entry<K,V>> entrySet()->获取Map集合中的键值对,转存到Set集合中
public class Demo04HashMap {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("猪八", "嫦娥");
map.put("猪八", "高翠兰");
map.put("后裔","嫦娥");
map.put("二郎神","嫦娥");
map.put("唐僧","女儿国国王");
/*
Set集合中保存的都是"结婚证"-> Map.Entry
我们需要将"结婚证"从set集合中遍历出来
*/
Set<Map.Entry<String, String>> set = map.entrySet();
for (Map.Entry<String, String> entry : set) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+"..."+value);
}
}
}
4.Map存储自定义对象时如何去重复
public class Person {
private String name;
private Integer age;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name) && Objects.equals(age, person.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Demo05HashMap {
public static void main(String[] args) {
HashMap<Person, String> map = new HashMap<>();
map.put(new Person("yy",18),"河北省");
map.put(new Person("xx",26),"日本");
map.put(new Person("zz",18),"北京市");
System.out.println(map);
}
}
如果key为自定义类型,去重复的话,重写hashCode和equals方法,去重复过程和set一样一样的
因为set集合的元素到了底层都是保存到了map的key位置上
第二章.哈希表结构存储过程
1.HashMap底层数据数据结构:哈希表
2.jdk7:哈希表 = 数组+链表
jdk8:哈希表 = 数组+链表+红黑树
3.
先算哈希值,此哈希值在HashMap底层经过了特殊的计算得出
如果哈希值不一样,直接存
如果哈希值一样,再去比较内容,如果内容不一样,也存
如果哈希值一样,内容也一样,直接去重复(后面的value将前面的value覆盖)
哈希值一样,内容不一样->哈希冲突(哈希碰撞)
4.要知道的点:
a.在不指定长度时,哈希表中的数组默认长度为16,HashMap创建出来,一开始没有创建长度为16的数组
b.什么时候创建的长度为16的数组呢?在第一次put的时候,底层会创建长度为16的数组
c.哈希表中有一个数据加[加载因子]->默认为0.75(加载因子)->代表当元素存储到百分之75的时候要扩容了->2倍
d.如果对个元素出现了哈希值一样,内容不一样时,就会在同一个索引上以链表的形式存储,当链表长度达到8并且当前数组长度>=64时,链表就会改成使用红黑树存储
如果后续删除元素,那么在同一个索引位置上的元素个数小于6,红黑树会变回链表
e.加入红黑树目的:查询快
外面笔试时可能会问到的变量
default_initial_capacity:HashMap默认容量 16
default_load_factor:HashMap默认加载因子 0.75f
threshold:扩容的临界值 等于 容量*0.75 = 12 第一次扩容
treeify_threshold:链表长度默认值,转为红黑树:8
min_treeify_capacity:链表被树化时最小的数组容量:64
1.问题:哈希表中有数组的存在,但是为啥说没有索引呢?
哈希表中虽然有数组,但是set和map却没有索引,因为存数据的时候有可能在同一个索引下形成链表,如果2索引上有一条链表,那么我们要是按照索引2获取,咱们获取哪个元素呢?所以就取消了按照索引操作的机制
2.问题:为啥说HashMap是无序的,LinkedHashMap是有序的呢?
原因:HashMap底层哈希表为单向链表
LinkedHashMap底层在哈希表的基础上加了一条双向链表
1.HashMap无参数构造方法的分析
//HashMap中的静态成员变量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
解析:使用无参数构造方法创建HashMap对象,将加载因子设置为默认的加载因子,loadFactor=0.75F。
2.HashMap有参数构造方法分析
HashMap(int initialCapacity, float loadFactor) ->创建Map集合的时候指定底层数组长度以及加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);//10
解析:带有参数构造方法,传递哈希表的初始化容量和加载因子
- 如果initialCapacity(初始化容量)小于0,直接抛出异常。
- 如果initialCapacity大于最大容器,initialCapacity直接等于最大容器
- MAXIMUM_CAPACITY = 1 << 30 是最大容量 (1073741824)
- 如果loadFactor(加载因子)小于等于0,直接抛出异常
- tableSizeFor(initialCapacity)方法计算哈希表的初始化容量。
- 注意:哈希表是进行计算得出的容量,而初始化容量不直接等于我们传递的参数。
3.tableSizeFor方法分析
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
8 4 2 1规则->无论指定了多少容量,最终经过tableSizeFor这个方法计算之后,都会遵循8421规则去初始化列表容量为了存取高效,尽量较少碰撞
解析:该方法对我们传递的初始化容量进行位移运算,位移的结果是 8 4 2 1 码
- 例如传递2,结果还是2,传递的是4,结果还是4。
- 例如传递3,结果是4,传递5,结果是8,传递20,结果是32。
4.Node 内部类分析
哈希表是采用数组+链表的实现方法,HashMap中的内部类Node非常重要,证明HashSet是一个单向链表
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
解析:内部类Node中具有4个成员变量
- hash,对象的哈希值
- key,作为键的对象
- value,作为值得对象(讲解Set集合,不牵扯值得问题)
- next,下一个节点对象
5.存储元素的put方法源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
解析:put方法中调研putVal方法,putVal方法中调用hash方法。
- hash(key)方法:传递要存储的元素,获取对象的哈希值
- putVal方法,传递对象哈希值和要存储的对象key
6.putVal方法源码
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
解析:方法中进行Node对象数组的判断,如果数组是null或者长度等于0,那么就会调研resize()方法进行数组的扩容。
7.resize方法的扩容计算
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
解析:计算结果,新的数组容量=原始数组容量<<1,也就是乘以2。
8.确定元素存储的索引
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
解析:i = (数组长度 - 1) & 对象的哈希值,会得到一个索引,然后在此索引下tab[i],创建链表对象。
不同哈希值的对象,也是有可能存储在同一个数组索引下。
其中resize()扩容的方法,默认是16
tab[i] = newNode(hash, key, value, null);->将元素放在数组中 i就是索引
i = (n - 1) & hash
0000 0000 0000 0000 0000 0000 0000 1111->15
& 0&0=0 0&1=0 1&1=1
0000 0000 0000 0001 0111 1000 0110 0011->96355
--------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0011->3
0000 0000 0000 0000 0000 0000 0000 1111->15
& 0&0=0 0&1=0 1&1=1
0000 0000 0001 0001 1111 1111 0001 0010->1179410
--------------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0010->2
9.遇到重复哈希值的对象
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
解析:如果对象的哈希值相同,对象的equals方法返回true,判断为一个对象,进行覆盖操作。
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
解析:如果对象哈希值相同,但是对象的equals方法返回false,将对此链表进行遍历,当链表没有下一个节点的时候,创建下一个节点存储对象.
第三章.TreeSet
1.概述:TreeSet是Set的实现类
2.特点:
a.对元素进行排序
b.无索引
c.不能存null
d.线程不安全
e.元素唯一
3.数据结构:红黑树
构造:
TreeSet() -> 构造一个新的空 set,该 set 根据其元素的自然顺序进行排序 -> ASCII
TreeSet(Comparator<? super E> comparator)构造一个新的空 TreeSet,它根据指定比较器进行排序
第四章.TreeMap
1.概述:TreeMap是Map的实现类
2.特点:
a.对key进行排序
b.无索引
c.key唯一
d.线程不安全
e.不能存null
3.数据结构:红黑树
构造:
TreeMap() -> 使用键的自然顺序构造一个新的、空的树映射 -> ASCII
TreeMap(Comparator<? super E> comparator)构造一个新的、空的树映射,该映射根据给定比较器进行排序
第五章.Hashtable和Vector集合(了解)
1.Hashtable集合
1.概述:Hashtable是Map的实现类
2.特点:
a.key唯一,value可重复
b.无序
c.无索引
d.线程安全
e.不能存储null键,null值
3.数据结构:哈希表
HashMap和Hashtable区别:
相同点:元素无序,无索引,key唯一
不同点:HashMap线程不安全,Hashtable线程安全
HashMap可以存储null键null值;Hashtable不能
2.Vector集合
1.概述:Vector是List接口的实现类
2.特点:
a.元素有序
b.有索引
c.元素可重复
d.线程安全
3.数据结构:数组
4.源码分析:
a.如果用空参构造创建对象,数组初始容量为10,如果超出范围,自动扩容,2倍
b.如果用有参构造创建对象,如果超出了范围,自动扩容,扩的是老数组长度+指定的容量增强
Vector底层源码分析
Vector() 构造一个空向量,使其内部数据数组的大小为 10,其标准容量增量为零
Vector(int initialCapacity, int capacityIncrement)使用指定的初始容量和容量增量构造一个空的向量
Vector<String> vector = new Vector<>();
public Vector() {
this(10);
}
public Vector(int initialCapacity->10) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity->10, int capacityIncrement->0) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];//长度为0的数组
this.capacityIncrement = capacityIncrement;//0
}
=====================================================
vector.add("李四");-> 假设李四是第11个元素
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity->11) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity->11);
}
private void grow(int minCapacity->11) {
// overflow-conscious code
int oldCapacity = elementData.length;//10
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);//10+10=20
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
Vector<String> vector = new Vector<>(10,5);
public Vector(int initialCapacity->10, int capacityIncrement->5) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;//5
}
======================================================
vector.add("李四");-> 假设李四是第11个元素
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity->11) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity->11) {
// overflow-conscious code
int oldCapacity = elementData.length;//10
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
第六章.Properties集合(属性集)
1.概述:Properties 继承自 Hashtable
2.特点:
a.key唯一,value可重复
b.无序
c.无索引
d.线程安全
e.不能存null键,null值
f.Properties的key和value类型默认为String
3.数据结构:哈希表
4.特有方法:
Object setProperty(String key, String value) -> 存键值对
String getProperty(String key) ->根据key获取value的
Set<String> stringPropertyNames() -> 获取所有的key,保存到set集合中,相当于keySet方法
void load(InputStream inStream) -> 将流中的数据加载到Properties集合中(IO部分讲)
第七章.集合嵌套
1.List嵌套List
public class Demo01ListInList {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("杨过");
list1.add("小龙女");
list1.add("尹志平");
ArrayList<String> list2 = new ArrayList<>();
list2.add("张三");
list2.add("李四");
list2.add("王五");
/*
list中的元素是两个 ArrayList<String>
所以泛型也应该是 ArrayList<String>
*/
ArrayList<ArrayList<String>> list = new ArrayList<>();
list.add(list1);
list.add(list2);
/*
先遍历大集合,将两个小集合遍历出来
再遍历两个小集合,将元素获取出来
*/
for (ArrayList<String> arrayList : list) {
for (String s : arrayList) {
System.out.println(s);
}
}
}
}
2.List嵌套Map
public class Demo02ListInMap {
public static void main(String[] args) {
//1.创建两个map集合
HashMap<Integer, String> map1 = new HashMap<>();
map1.put(1,"张三");
map1.put(2,"李四");
map1.put(3,"王五");
HashMap<Integer, String> map2 = new HashMap<>();
map2.put(1,"黄晓明");
map2.put(2,"杨颖");
map2.put(3,"刘德华");
//2.创建一个存储map集合的list集合
ArrayList<HashMap<Integer, String>> list = new ArrayList<>();
list.add(map1);
list.add(map2);
//3.先遍历list集合,再遍历map集合
for (HashMap<Integer, String> map : list) {
Set<Map.Entry<Integer, String>> set = map.entrySet();
for (Map.Entry<Integer, String> entry : set) {
System.out.println(entry.getKey()+"..."+entry.getValue());
}
}
}
}
3.Map嵌套Map
public class Demo03MapInMap {
public static void main(String[] args) {
//1.创建两个map集合
HashMap<Integer, String> map1 = new HashMap<>();
map1.put(1,"张三");
map1.put(2,"李四");
HashMap<Integer, String> map2 = new HashMap<>();
map2.put(1,"王五");
map2.put(2,"赵六");
HashMap<String, HashMap<Integer, String>> map = new HashMap<>();
map.put("javase",map1);
map.put("javaee",map2);
Set<Map.Entry<String, HashMap<Integer, String>>> set = map.entrySet();
for (Map.Entry<String, HashMap<Integer, String>> entry : set) {
HashMap<Integer, String> hashMap = entry.getValue();
Set<Integer> set1 = hashMap.keySet();
for (Integer key : set1) {
System.out.println(key+"..."+hashMap.get(key));
}
}
}
}
模块21:IO流1
第一章.File类
1.File类
计算机常识:
1.以.jpg结尾的一定是图片吗?
不一定,有可能是文件夹
2.什么是文本文档?
用记事本打开,人能看懂的文件 -> txt html css
3.E:\Idea\io\1.jpg 中的1.jpg的父路径是谁?
E:\Idea\io
4.分隔符:
a.路径名称分隔符:
windows: \
linux: /
b.路径分隔符:一个路径和其他路径之间的分隔符“;”
1.概述:文件和目录(文件夹)路径名的抽象表示
2.简单理解:
我们在创建File对象的时候,需要传递一个路径,这个路径定为到哪个文件或者文件夹上,我们的File就代表哪个对象
File file = new File("E:\Idea\io\1.jpg")
2.File的静态成员
static String pathSeparator:与系统有关的路径分隔符,为了方便,它被表示为一个字符串。
static String separator:与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。
public class Demo01File {
public static void main(String[] args) {
file01();
file02();
}
/*
将来写代码如何正确编写一个路径用java代码
*/
private static void file02() {
String path1 = "E:\Idea\io";
System.out.println(path1);
System.out.println("==================");
//要求代码写完,一次编写,到处运行
String path2 = "E:"+File.separator+"Idea"+File.separator+"io";
System.out.println(path2);
}
private static void file01() {
//static String pathSeparator:与系统有关的路径分隔符,为了方便,它被表示为一个字符串。
String pathSeparator = File.pathSeparator;
System.out.println("pathSeparator = " + pathSeparator); // ;
//static String separator:与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。
String separator = File.separator;
System.out.println("separator = " + separator); // \
}
}
3.File的构造方法
File(String parent, String child) 根据所填写的路径创建File对象
parent:父路径
child:子路径
File(File parent, String child) 根据所填写的路径创建File对象
parent:父路径,是一个File对象
child:子路径
File(String pathname) 根据所填写的路径创建File对象
pathname:直接指定路径
public class Demo02File {
public static void main(String[] args) {
//File(String parent, String child) 根据所填写的路径创建File对象
//parent:父路径
//child:子路径
File file1 = new File("E:\Idea\io", "1.jpg");
System.out.println("file1 = " + file1);
//File(File parent, String child) 根据所填写的路径创建File对象
//parent:父路径,是一个File对象
//child:子路径
File parent = new File("E:\Idea\io");
File file2 = new File(parent, "1.jpg");
System.out.println("file2 = " + file2);
//File(String pathname) 根据所填写的路径创建File对象
//pathname:直接指定路径
File file3 = new File("E:\Idea\io\1.jpg");
System.out.println("file3 = " + file3);
}
}
我们创建File对象的时候,传递的路径可以是不存在的,但是传递不存在的路径
4.File的获取方法
String getAbsolutePath() -> 获取File的绝对路径->带盘符的路径
String getPath() ->获取的是封装路径->new File对象的时候写的啥路径,获取的就是啥路径
String getName() -> 获取的是文件或者文件夹名称
long length() -> 获取的是文件的长度 -> 文件的字节数
private static void file01() {
//String getAbsolutePath() -> 获取File的绝对路径->带盘符的路径
File file1 = new File("1.txt");
System.out.println("file1.getAbsolutePath() = " + file1.getAbsolutePath());
//String getPath() ->获取的是封装路径->new File对象的时候写的啥路径,获取的就是啥路径
File file2 = new File("io\1.txt");
System.out.println("file2.getPath() = " + file2.getPath());
//String getName() -> 获取的是文件或者文件夹名称
File file3 = new File("E:\Idea\io\1.jpg");
System.out.println("file3.getName() = " + file3.getName());
//long length() -> 获取的是文件的长度 -> 文件的字节数
File file4 = new File("E:\Idea\io\1.txt");
System.out.println("file4.length() = " + file4.length());
}
5.File的创建方法
boolean createNewFile() -> 创建文件
如果要创建的文件之前有,创建失败,返回false
如果要创建的文件之前没有,创建成功,返回true
boolean mkdirs() -> 创建文件夹(目录)既可以创建多级文件夹,还可以创建单级文件夹
如果要创建的文件夹之前有,创建失败,返回false
如果要创建的文件夹之前没有,创建成功,返回true
private static void file02() throws IOException {
/*boolean createNewFile() -> 创建文件
如果要创建的文件之前有,创建失败,返回false
如果要创建的文件之前没有,创建成功,返回true*/
File file1 = new File("E:\Idea\io\1.txt");
System.out.println("file1.createNewFile() = " + file1.createNewFile());
/*boolean mkdirs() -> 创建文件夹(目录)既可以创建多级文件夹,还可以创建单级文件夹
如果要创建的文件夹之前有,创建失败,返回false
如果要创建的文件夹之前没有,创建成功,返回true*/
File file2 = new File("E:\Idea\io\haha\heihei\hehe");
System.out.println("file2.mkdirs() = " + file2.mkdirs());
}
6.File类的删除方法
boolean delete()->删除文件或者文件夹
注意:
1.如果删除文件,不走回收站
2.如果删除文件夹,必须是空文件夹,而且也不走回收站
7.File类的判断方法
boolean isDirectory() -> 判断是否为文件夹
boolean isFile() -> 判断是否为文件
boolean exists() -> 判断文件或者文件夹是否存在
private static void file04() {
File file = new File("E:\Idea\io\1.txt");
// boolean isDirectory() -> 判断是否为文件夹
System.out.println("file.isDirectory() = " + file.isDirectory());
// boolean isFile() -> 判断是否为文件
System.out.println("file.isFile() = " + file.isFile());
// boolean exists() -> 判断文件或者文件夹是否存在
System.out.println("file.exists() = " + file.exists());
}
8.File的遍历方法
String[] list() -> 遍历指定的文件夹,返回的是String数组
File[] listFiles()-> 遍历指定的文件夹,返回的是File数组 ->这个推荐使用
细节:listFiles方法底层还是list方法
调用list方法,遍历文件夹,返回一个Stirng数组,遍历数组,将数组中的内容一个一个封装到File对象中,然后再将File对象放到File数组中
private static void file05() {
File file = new File("E:\Idea\io\aa");
//String[] list() -> 遍历指定的文件夹,返回的是String数组
String[] list = file.list();
for (String s : list) {
System.out.println(s);
}
//File[] listFiles()-> 遍历指定的文件夹,返回的是File数组 ->这个推荐使用
System.out.println("==============");
File[] files = file.listFiles();
for (File file1 : files) {
System.out.println(file1);
}
}
9.相对路径和绝对路径
1.绝对路径:从盘符开始写的路径
E:\idea\io\1.txt
2.相对路径:不从盘符名开始写的路径
3.针对idea中写相对路径:
a.口诀:哪个路径是参照路径,哪个路径就可以省略不写,剩下的就是在idea中的相对路径写法
在idea中参照路径其实就是当前project的绝对路径
b.比如:在module21下创建了一个1.txt
先找1.txt的绝对路径:E:\Idea\idea2022\workspace\javase\module21\1.txt
再找参照路径:E:\Idea\idea2022\workspace\javase
参照路径可以省略:module21\1.txt
4.总结:
在idea中写的相对路径,其实就是从模块名开始写
5.注意:
如果直接写一个文件名1.txt,它所在的位置默认是在当前project下
第二章.字节流
1.IO流介绍以及输入输出以及流向的介绍
1.单词:
Output:输出
Input:输入
write:写数据
read:读数据
2.IO流:
将一个设备上的数据传输到另外一个设备上,称之为IO流技术
3.为啥要学IO流呢?
之前学了一个集合以及数组,可以保存数据,但是这两个都是临时存储(代码运行完毕,集合和数组会从内存中消失,从而数据就不存在了),所以集合和数组达不到永久保存的目的,我们希望咱们的数据永久保存起来,所以我们就可以将数据保存到硬盘上,此时我们就可以随时想拿到硬盘上的数据就随时拿
而且我们将来传输数据,必然要用到输入,输出动作
2.IO流的流向_针对se阶段的IO
输入流:将数据从硬盘上读到内存中 Input
输出流:从内存出发,将数据写到硬盘上 Output
要是从电脑和电脑之间做数据传输,就是相对的
发数据一方 : 输出
接数据一方 : 输入
3.IO流分类
字节流:万能流,一切皆字节
字节输出流: OutputStream 抽象类
字节输入流: InputStream 抽象类
字符流:专门操作文本文档
字符输出流:Writer 抽象类
字符输入流:Reader 抽象类
4.FileOutputStream的介绍以及方法的简单介绍
1.概述:字节输出流,OutputStream 是一个抽象类
子类: FileOutputStream
2.作用:往硬盘上写数据
3.构造:
FileOutputStream(File file)
FileOutputStream(String name)
4.特点:
a.指定的文件如果没有,输出流会自动创建
b.每执行一次,默认都会创建一个新的文件,覆盖老文件
5.方法:
void write(int b) 一次写一个字节
void write(byte[] b) 一次写一个字节数组
void write(byte[] b, int off, int len) 一次写一个字节数组一部分
b:写的数组
off:从数组的哪个索引开始写
len:写多少个
void close() -> 关闭资源
/*
void write(int b) 一次写一个字节
*/
private static void method01() throws IOException {
FileOutputStream fos = new FileOutputStream("module21\1.txt");
fos.write(97);
fos.close();
}
/*
void write(byte[] b) 一次写一个字节数组
*/
private static void method02()throws IOException {
FileOutputStream fos = new FileOutputStream("module21\1.txt");
byte[] bytes = {97,98,99,100,101,102,103};
fos.write(bytes);
fos.close();
}
/*
void write(byte[] b) 一次写一个字节数组
*/
private static void method04()throws IOException {
FileOutputStream fos = new FileOutputStream("module21\1.txt");
//byte[] bytes = "abc".getBytes();
fos.write("abcde".getBytes());
fos.close();
}
1.字节流的续写追加:
FileOutputStream(String name, boolean append)
append:true -> 会实现续写追加,文件不覆盖了
2.换行:
a.windows: \r\n -> 占2个字节 \n
b.linux: \n
c.mac os : \r
5.FileInputStream的介绍以及方法的使用
1.概述:字节输入流 InputStream ,是一个抽象类
子类:FileInputStream
2.作用:读数据,将数据从硬盘上读到内存中来
3.构造:
FileInputStream(File file)
FileInputStream(String path)
4.方法:
int read() 一次读一个字节,返回的是读取的字节
int read(byte[] b) 一次读取一个字节数组,返回的是读取的字节个数
int read(byte[] b, int off, int len) 一次读取一个字节数组一部分,返回的是读取的字节个数
void close() 关闭资源
6.一次读取一个字节
/*
int read() 一次读一个字节,返回的是读取的字节
*/
private static void method01()throws Exception {
FileInputStream fis = new FileInputStream("module21\1.txt");
//int data1 = fis.read();
//System.out.println(data1);
//int data2 = fis.read();
//System.out.println(data2);
//int data3 = fis.read();
//System.out.println(data3);
//int data4 = fis.read();
//System.out.println(data4);// -1
//int data5 = fis.read();
//System.out.println(data5);//-1
System.out.println("=================");
//定义一个变量,接收读取到的字节
int len;
while((len = fis.read())!=-1){
System.out.println((char)len);
}
fis.close();
}
问题1:一个流对象,读完之后,就不要再读了;除非重新new一个新的对象
问题2:流关闭之后,流对象不能继续使用了
Exception in thread "main" java.io.IOException: Stream Closed
at java.base/java.io.FileInputStream.read0(Native Method)
at java.base/java.io.FileInputStream.read(FileInputStream.java:228)
at com.atguigu.c_input.Demo01FileInputStream.method01(Demo01FileInputStream.java:38)
at com.atguigu.c_input.Demo01FileInputStream.main(Demo01FileInputStream.java:7)
7.读取-1的问题
每个文件末尾都会有一个"结束标记",这个"结束标记"我们看不见,摸不着
而read()方法规定,如果读到了文件的结束标记,方法直接返回-1
8.一次读取一个字节数组以及过程
/*
int read(byte[] b) 一次读取一个字节数组,返回的是读取的字节个数
*/
private static void method02()throws Exception {
FileInputStream fis = new FileInputStream("module21\1.txt");
/*
创建一个数组:byte[]
1.创建的数组相当于一个临时存储区域,我们要读取的内容会临时保存到数组中
然后我们再从数组中将数据获取
2.数组长度定为多少,每次读取多少个,一般情况下数组长度定为1024或者1024的倍数
如果剩下的字节不够数组长度了,那么就最后有多少读多少
*/
byte[] bytes = new byte[2];
//int len1 = fis.read(bytes);
/*System.out.println(len1);//2
System.out.println(new String(bytes,0,len1));
System.out.println("===============");
int len2 = fis.read(bytes);
System.out.println(len2);//2
System.out.println(new String(bytes,0,len2));
System.out.println("===============");
int len3 = fis.read(bytes);
System.out.println(len3);//1
System.out.println(new String(bytes,0,len3));*/
//定义一个变量len,接收读取的字节个数
int len;
while((len = fis.read(bytes))!=-1){
//System.out.println(new String(bytes,0,len));
System.out.println(new String(bytes));
}
fis.close();
}
9.字节流实现图片复制分析
10.字节流实现图片复制代码实现
public class Demo01CopyFile {
public static void main(String[] args)throws Exception {
//1.创建FileInputStream
FileInputStream fis = new FileInputStream("E:\Idea\io\24.jpg");
//2.创建一个FileOutputStream,将读取的图片写到指定的位置
FileOutputStream fos = new FileOutputStream("E:\Idea\io\大姐.jpg");
//3.定义一个数组
byte[] bytes = new byte[1024];
//4.边读边写
int len;
while((len = fis.read(bytes))!=-1){
fos.write(bytes,0,len);//读多少个,写多少个
}
//5.关流 先开后关
fos.close();
fis.close();
}
}
第三章.字符流
1.字节流读取中文的问题
1.注意:
字节流是万能流,更侧重于文件复制,但是尽量不要边读边看
2.原因:
UTF-8:一个汉字占3个字节
GBK:一个中文占2个字节
如果按照字节读取,每次读取的字节没有构成一个汉字的字节数,就直接输出,汉字是显示不了的
3.解决:
将文本文档中的内容,按照字符去操作
话说回来:
1.按照字符去操作编码也要一致,如果不一致,照样会乱码
2.按照字节流去操作即使编码一致,边读边看,也有可能乱码
2.FileReader的介绍以及使用
字符流专门操作文本文档的,但是复制操作不要用字符流,要用字节流
1.概述:字符输入流 -> Reader -> 是一个抽象类
子类:FileReader
2.作用:将文本文档中的内容读取到内存中来
3.构造:
FileReader(File file)
FileReader(String path)
4.方法:
int read() -> 一次读取一个字符,返回的是读取字符对应的int值
int read(char[] cbuf) -> 一次读取一个字符数组,返回的是读取个数
int read(char[] cbuf, int off, int len) -> 一次读取一个字符数组一部分,返回的是读取个数
cbuf:读取的数组
off:从数组的哪个索引开始读
len:读多少个
void close() -> 关闭资源
private static void method01() throws IOException {
FileReader fr = new FileReader("module21\1.txt");
int len;
while((len = fr.read())!=-1){
System.out.println((char) len);
}
fr.close();
}
private static void method01() throws IOException {
FileReader fr = new FileReader("module21\1.txt");
int len;
while((len = fr.read())!=-1){
System.out.println((char) len);
}
fr.close();
}
private static void method02()throws Exception {
FileReader fr = new FileReader("module21\1.txt");
char[] chars = new char[2];
int len;
while((len = fr.read(chars))!=-1){
System.out.println(new String(chars,0,len));
}
fr.close();
}
3.FileWriter的介绍以及使用
1.概述:字符输出流 -> Writer -> 抽象类
子类:FileWriter
2.作用:将数据写到文件中
3.构造:
FileWriter(File file)
FileWriter(String fileName)
FileWriter(String fileName, boolean append) -> 追加,续写
4.方法:
void write(int c) -> 一次写一个字符
void write(char[] cbuf) 一次写一个字符数组
void write(char[] cbuf, int off, int len) 一次写一个字符数组一部分
void write(String str) 直接写一个字符串
void flush() :将缓冲区中的数据刷到文件中
void close() 关流
5.注意:FileWriterr底层自带一个缓冲区,我们写的数据会先保存到缓冲区中,所以我们需要将缓冲区中的数据刷到文件中
public class Demo01FileWriter {
public static void main(String[] args)throws Exception {
FileWriter fw = new FileWriter("module21\2.txt");
fw.write("千山鸟飞绝\r\n");
fw.write("万径人踪灭\r\n");
fw.write("孤舟蓑笠翁\r\n");
fw.write("独钓寒江雪\r\n");
//fw.flush();
fw.close();
}
}
4.FileWriter的刷新功能和关闭功能
flush():将缓冲区中的数据刷到文件中,后续流对象还能继续使用
close():先刷新后关闭,后续流对象不能使用了
5.IO异常处理的方式
public class Demo01Exception {
public static void main(String[] args) {
FileWriter fw = null;
try{
fw = new FileWriter("module21\3.txt");
fw.write("你好");
}catch (Exception e){
e.printStackTrace();
}finally {
//如果fw不为null,证明new出来了所以需要close;相反不需要close
if (fw!=null){
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
6.JDK7之后io异常处理方式
1.格式:
try(IO对象){
可能出现异常的代码
}catch(异常类型 对象名){
处理异常
}
2.注意:
以上格式处理IO异常,会自动关流
public class Demo02Exception {
public static void main(String[] args) {
try(FileWriter fw = new FileWriter("module21\4.txt");){
fw.write("你好");
}catch (Exception e){
e.printStackTrace();
}
}
}