一 Map集合(会)
import java.util.*; /** * @Author 千锋⼤数据教学团队 * @Company 千锋好程序员⼤数据 * @Description Map API */ public class MapUsage { public static void main(String[] args) { // 1. 实例化⼀个Map集合的实现类对象,并向上转型为接⼝类型。 Map<String, String> map = new HashMap<>(); // 2. 向集合中插⼊数据 String value = map.put("name", "xiaoming"); System.out.println(value); // 由于第⼀次添加这个键值 对,集合中没有被覆盖的值,因此返回null String value2 = map.put("name", "xiaobai"); System.out.println(value2); // 这⾥是第⼆次设置name的 值,会⽤xiaobai覆盖掉xiaoming,因此返回xiaoming // 3. 向集合中插⼊数据 String value3 = map.putIfAbsent("name", "xiaohong"); System.out.println(value3); // 这⾥返回的是集合中已经存 在的这个键对应的值 String value4 = map.putIfAbsent("age", "20"); System.out.println(value4); // 由于这个集合中原来是没有 age键存在的,所以返回的是null // 4. 将⼀个Map集合中所有的键值对添加到当前的集合中 Map<String, String> tmp = new HashMap<>(); tmp.put("height", "177"); tmp.put("weight", "65"); tmp.put("age", "30"); map.putAll(tmp); // 5. 删除:通过键,删除⼀个键值对,并返回这个被删除的键值对中的 值。 String value5 = map.remove("weight"); System.out.println(value5); // 6. 删除 boolean value6 = map.remove("age", "30"); System.out.println(value6); // 7. 清空集合 // map.clear(); // 8. 修改集合中的某⼀个键值对(通过键,修改值) System.out.println(value7); // 返回被覆盖的值 String value8 = map.replace("age", "30"); System.out.println(value8); // 由于map中没有age键,因此这个返回null // 9. 修改: 只有当key和oldValue是匹配的情况下,才会将值修改成 newValue。 boolean value9 = map.replace("name", "xiaohei", "xiaohong"); System.out.println(value9); // 10. 对集合中的元素进⾏批量的替换 // 将集合中的每⼀个键值对,带⼊到BiFunction的⽅法中,使⽤接 ⼝⽅法的返回值替换集合中原来的值。 map.replaceAll((k, v) -> { if (k.equals("height")) { return v + "cm"; } return v; }); // 11. 通过键获取值。 String value10 = map.get("name1"); System.out.println(value10); // 12. 通过键获取值,如果这个键不存在,则返回默认的值。 String value11 = map.getOrDefault("name1","aaa"); System.out.println(value11); // 13. 判断是否包含某⼀个键 boolean value12 = map.containsKey("height"); System.out.println(value12); boolean value13 = map.containsValue("177"); System.out.println(value13); // 14. 获取由所有的键组成的Set集合 Set<String> keys = map.keySet(); // 获取由所有的值组成的Collection集合 Collection<String> values = map.values(); System.out.println(map); } }
1.1 Map集合的遍历(会)
.使⽤keySet进⾏遍历 1. 可以使⽤keySet()⽅法获取到集合中所有的键。 2. 遍历存储了所有的键的集合,依次通过键获取值。 /** * 1. 使⽤keySet进⾏遍历 * @param map 需要遍历的集合 */ private static void keyset(Map<String, String> map) { // 1. 获取存储了所有的键的集合 Set<String> keys = map.keySet(); // 2. 遍历这个Set集合 for (String key : keys) { // 2.1. 通过键获取值 String value = map.get(key); // 2.2. 展示⼀下键和值 System.out.println("key = " + key + ", value = " + value); } } .使⽤forEach⽅法 这个forEach⽅法, 并不是Iterable接⼝中的⽅法。 是Map接⼝中定义的⼀个⽅法。 从功能上讲, 与Iterable中的⽅法差不多。 只是在参数部分有区别。 default void forEach(BiConsumer<? super K, ? super V> action) /** * 2. 使⽤forEach进⾏遍历 * @param map 需要遍历的集合 */ private static void forEach(Map<String, String> map) { map.forEach((k, v) -> { // k: 遍历到的每⼀个键 // v: 遍历到的每⼀个值 System.out.println("key = " + k + ", value = " + v); }); } .使⽤EntrySet进⾏遍历 Entry<K, V>: 是Map中的内部静态接⼝, ⼀个Entry对象我们称为⼀个实体,⽤来描述集合中的每⼀个键值对。 /** * 3. 使⽤entrySet进⾏遍历 * @param map 需要遍历的集合 */ private static void entrySet(Map<String, String> map) { // 1. 获取⼀个存储有所有的Entry的⼀个Set集合 Set<Map.Entry<String, String>> entries = map.entrySet(); // 2. 遍历Set集合 for (Map.Entry<String, String> entry : entries) { // 2.1. 获取键 String key = entry.getKey(); // 2.2. 获取值 String value = entry.getValue(); // 2.3. 展示 System.out.println("key = " + key + ", value = " + value); //通过setValue可以去修改原始map的值 //映射项(键-值对)。Map.entrySet ⽅法返回映射的 collection 视图,其中的元素属于此类。 //获得映射项引⽤的唯⼀ ⽅法是通过此 collection 视图的迭代器来实 现。这些 Map.Entry 对象仅 //在迭代期间有效;更正式地说,如果在迭代器返回项之后修改了底层映 射,则 //某些映射项的⾏为是不确定的,除了通过 setValue 在映射项上执⾏ 操作之外。 //entry.setValue("hello"); } }
二 线程
1 为什么要使⽤线程?
在程序中完成某⼀个功能的时候,我们会将他描述成任务,这个任务需要在线程中完成.
2 串⾏与并发
如果在程序中,有多个任务需要被处理,此时的处理⽅式可以有串⾏和并发: 串⾏(同步):所有的任务,按照⼀定的顺序,依次执⾏。如果前⾯的任务没有 执⾏结束,后⾯的任务等待。 并发(异步):将多个任务同时执⾏,在⼀个时间段内,同时处理多个任务。
3 进程和线程
进程, 是对⼀个程序在运⾏过程中, 占⽤的各种资源的描述。 线程, 是进程中的⼀个最⼩的执⾏单元。 其实, 在操作系统中, 最⼩的任务执⾏单元并不是线程, ⽽是句柄。 只不过句柄过⼩, 操作起来⾮常的麻烦, 因此线程就是我们可控的最⼩的任务执⾏单元。 进程和线程的异同: 相同点: 进程和线程都是为了处理多个任务并发⽽存在的。 不同点: 进程之间是资源不共享的, ⼀个进程中不能访问另外⼀个进程中的数据。 ⽽线程之间是资源共享的, 多个线程可以共享同⼀个数据。 也正因为线程之间是资源共享的, 所以会出现临界资源的问题。 进程和线程的关系: ⼀个进程, 在开辟的时候, 会⾃动的创建⼀个线程, 来处理这个进程中的任务。 这个线程被称为是主线程。 在程序运⾏的过程中, 还可以开辟其他线程, 这些被开辟 出来的其他线程, 都是⼦线程。也就是说, ⼀个进程中, 是可以包含多个线程。 ⼀个进程中的某⼀个线程崩溃了, 只要还有其他线程存在, 就不会影响整个进程的执⾏。 但是如果⼀个进程中, 所有的线程都执⾏结束了, 那么这个进程也就终⽌了。 总结 程序:⼀个可执⾏的⽂件 进程:⼀个正在运⾏的程序.也可以理解成在内存中开辟了⼀块⼉空间 线程:负责程序的运⾏,可以看做⼀条执⾏的通道或执⾏单元,所以我们通常将进程 的⼯作理解成线程的⼯作 进程中可不可以没有线程? 必须有线程,⾄少有⼀个. 当有⼀个线程的时候我们称为单线程(唯⼀的线程就是主线程). 当有⼀个以上的线程同时存在的时候我们称为多线程.
4 线程的⽣命周期(会)
4.1 线程的状态:
线程的⽣命周期, 指的是⼀个线程对象, 从最开始的创建, 到最后的销毁, 中间所经历的过程。 在这个过程中, 线程对象处于不同的状态。 New: 新⽣态, ⼀个线程对象刚被实例化完成的时候, 就处于这个状态。 Runnable: 就绪态, 处于这个状态的线程, 可以参与CPU时间⽚的争抢。 Run: 运⾏态, 某⼀个线程抢到了CPU时间⽚, 可以执⾏这个线程中的逻辑 Block: 阻塞态, 线程由于种种原因, 暂时挂起, 处于阻塞(暂停)状态。 这个状态的线程, 不参与CPU时间⽚的争抢。 Dead: 死亡态, 线程即将被销毁。
4.2 . 线程的⽣命周期图
5 理解多线程(会)
简单理解(cpu单核):从宏观上看,线程有并发执⾏,从微观上看,并没有,在线程完成任务时,实际⼯作的是cpu,我们将cpu⼯作描述为时间⽚(单次获取cpu的时间,⼀般在⼏⼗毫秒).cpu只有⼀个,本质上同⼀时间只能做⼀件事,因为cpu单次时间⽚很短,短到⾁眼⽆法区分,所以当cpu在多个线程之间快速切换时,宏观上给我们的感觉是多件事同时在执⾏. 注意: 1.cpu是随机的,线程之间本质上默认是抢cpu的状态,谁抢到了谁就获得了时间⽚,就⼯作,所以多个线程的⼯作也是默认随机的. 2.在使⽤多线程时,并不是线程数越多越好,本质上⼤家共同使⽤⼀个cpu,完成任务的时间并没有减少.要根据实际情况创建线程,多线程是为了实现同⼀时间完成多件事情的⽬的.⽐如我们⽤⼿机打开⼀个app时,需要滑动界⾯浏览,同时界⾯的图⽚需要下载,对于这两个功能最好同时进⾏,这时可以使⽤多线程. 示例代码: public class Demo4 { //就是主线程的任务区 public static void main(String[] args) { new Test(); /* * ⼿动运⾏垃圾回收器 */ System.gc(); System.out.println("main"); } } class Test{ @Override /* * 重写finalize⽅法 */ protected void finalize() throws Throwable { System.out.println("finalize"); } }
6 创建线程(会)
6.1原因分析
默认情况下,主线程和垃圾回收线程都是由系统创建的,但是我们需要完成⾃⼰的功能,所以需要创建⾃⼰的线程 java将线程⾯向对象了,形成的类就是Thread,在Thread类内部执⾏任务的⽅法叫run()
6.2 . 继承Thread类
继承⾃Thread类, 做⼀个Thread的⼦类。 在⼦类中, 重写⽗类中的run⽅法, 在这个重写的⽅法中, 指定这个线程需要处理的任务。 Thread.currentThread() : 可以⽤在任意的位置, 获取当前的线程。 如果是Thread的⼦类, 可以在⼦类中, 使⽤this获取到当前的线程。 当我们⼿动调⽤run的时候,他失去了任务区的功能,变成了⼀个普通的⽅法. 当run作为⼀个普通⽅法时,内部对应的线程跟调⽤他的位置保持⼀致. 结果分析: 主线程和两个⼦线程之间是随机打印的,他们是抢cpu的关系. 通过创建Thread⼦类的⽅式实现功能,线程与任务绑定在了⼀起,操作不⽅法我们可以将任务从线程中分离出来,哪个线程需要⼯作,就将任务交给谁,操作⽅便,灵活-使⽤Runnable接⼝ 示例代码: public class Demo5 { public static void main(String[] args) {//为了⽅便研究,先暂时不 考虑垃圾回收线程. MyThread t1 = new MyThread("bing"); MyThread t2 = new MyThread("ying"); t1.start(); t2.start(); //⼿动调⽤run()⽅法 t1.run(); for (int i = 0; i <10; i++) { System.out.println(Thread.currentThread().getName()+" i:"+i); } } } class MyThread extends Thread{ String name1; public MyThread(String name1) { this.name1 = name1; } //任务区 //重写run⽅法,实现我们的功能.run就是我们的任务区 /* * Thread.currentThread():获取的是当前的线程 * Thread.currentThread().getName():线程的名字 */ @Override public void run() { for (int i = 0; i <10; i++) { System.out.println(this.name1+" "+Thread.currentThread().getName()+" i:"+i); } } }
6.3 使用Runnable接口
在Thread类的构造⽅法中, 有⼀个重载的构造⽅法, 参数是 Runnable 接⼝。 因此, 可以通过Runnable接⼝的实现类对象进⾏Thread对象的实例化。 这⾥Thread内部默认有⼀个run,⼜通过runnable传⼊⼀个run,为什么优先调⽤的是传⼊的run? 如果该线程是使⽤独⽴的 Runnable 运⾏对象构造的,则调⽤该 Runnable 对象的 run ⽅法;否则,该⽅法不执⾏任何操作并返回。 示例代码: public class Program { public static void main(String[] args) { // Runnable接⼝的匿名实现类 Runnable runnable = new Runnable() { @Override public void run() { System.out,println("⼦线程处理的逻辑"); } }; // 实例化线程对象 Thread thread = new Thread(runnable); } }
6.4 优缺点对比
继承的⽅式: 优点在于可读性⽐较强, 缺点在于不够灵活。 如果要定制⼀个线 public class Demo6 { public static void main(String[] args) { //创建4个线程代表4个售票员 //线程与任务不分离 // SubThread s1 = new SubThread(); // SubThread s2 = new SubThread(); // SubThread s3 = new SubThread(); // SubThread s4 = new SubThread(); // // //开启线程 // s1.start(); // s2.start(); // s3.start(); // s4.start(); //线程与任务分离测试 Ticket ticket = new Ticket(); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket);程, 就必须要继承⾃Thread类, 可能会影响原有的继承体系。 接⼝的⽅式: 优点在于灵活, 并且不会影响⼀个类的继承体系。 缺点在于可读性较差。 public class Demo6 { public static void main(String[] args) { //创建4个线程代表4个售票员 //线程与任务不分离 // SubThread s1 = new SubThread(); // SubThread s2 = new SubThread(); // SubThread s3 = new SubThread(); // SubThread s4 = new SubThread(); // // //开启线程 // s1.start(); // s2.start(); // s3.start(); // s4.start(); //线程与任务分离测试 Ticket ticket = new Ticket(); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); Thread t4 = new Thread(ticket); //开启线程 t1.start(); t2.start(); t3.start(); t4.start(); } } //线程与任务不分离 class SubThread extends Thread{ static int num = 20;//想让⼤家共⽤这个num @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName()+" num:"+ --num); } } } //线程与任务分离 class Ticket implements Runnable{ int num = 20;//想让⼤家共⽤这个num @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName()+" num:"+ --num); } } }