08-27-周三_15-08-08

This commit is contained in:
2025-08-27 15:08:08 +08:00
parent 79f3d5bb72
commit 41c4825c52
296 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,612 @@
# day01【Object类、常用API】
### 主要内容
* Object类
* Date类
* DateFormat类
* Calendar类
* System类
* StringBuilder类
* 包装类
### 教学目标
-[ ] 能够说出Object类的特点
-[ ] 能够重写Object类的toString方法
-[ ] 能够重写Object类的equals方法
-[ ] 能够使用日期类输出当前日期
-[ ] 能够使用将日期格式化为字符串的方法
-[ ] 能够使用将字符串转换成日期的方法
-[ ] 能够使用System类的数组复制方法
-[ ] 能够使用System类获取当前毫秒时刻值
-[ ] 能够说出使用StringBuilder类可以解决的问题
-[ ] 能够使用StringBuilder进行字符串拼接操作
-[ ] 能够说出8种基本类型对应的包装类名称
-[ ] 能够说出自动装箱、自动拆箱的概念
-[ ] 能够将字符串转换为对应的基本类型
-[ ] 能够将基本类型转换为对应的字符串
## 第一章 Object类
### 1.1 概述
`java.lang.Object`类是Java语言中的根类即所有类的父类。它中描述的所有方法子类都可以使用。在对象实例化的时候最终找的父类就是Object。
如果一个类没有特别指定父类那么默认则继承自Object类。例如
```java
public class MyClass /*extends Object*/ {
// ...
}
```
根据JDK源代码及Object类的API文档Object类当中包含的方法有11个。今天我们主要学习其中的2个
* `public String toString()`:返回该对象的字符串表示。
* `public boolean equals(Object obj)`:指示其他某个对象是否与此对象“相等”。
### 1.2 toString方法
#### 方法摘要
* `public String toString()`:返回该对象的字符串表示。
toString方法返回该对象的字符串表示其实该字符串内容就是对象的类型+@+内存地址值。
由于toString方法返回的结果是内存地址而在开发中经常需要按照对象的属性得到相应的字符串表现形式因此也需要重写它。
#### 覆盖重写
如果不希望使用toString方法的默认行为则可以对它进行覆盖重写。例如自定义的Person类
```java
public class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
// 省略构造器与Getter Setter
}
```
在IntelliJ IDEA中可以点击`Code`菜单中的`Generate...`,也可以使用快捷键`alt+insert`,点击`toString()`选项。选择需要包含的成员变量并确定。如下图所示:
![toString方法的自动重写](img\toString方法的自动重写.bmp)
> 小贴士: 在我们直接使用输出语句输出对象名的时候,其实通过该对象调用了其toString()方法。
>
### 1.3 equals方法
#### 方法摘要
* `public boolean equals(Object obj)`:指示其他某个对象是否与此对象“相等”。
调用成员方法equals并指定参数为另一个对象则可以判断这两个对象是否是相同的。这里的“相同”有默认和自定义两种方式。
#### 默认地址比较
如果没有覆盖重写equals方法那么Object类中默认进行`==`运算符的对象地址比较只要不是同一个对象结果必然为false。
#### 对象内容比较
如果希望进行对象的内容比较即所有或指定的部分成员变量相同就判定两个对象相同则可以覆盖重写equals方法。例如
```java
import java.util.Objects;
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
// 如果对象地址一样,则认为相同
if (this == o)
return true;
// 如果参数为空,或者类型信息不一样,则认为不同
if (o == null || getClass() != o.getClass())
return false;
// 转换为当前类型
Person person = (Person) o;
// 要求基本类型相等并且将引用类型交给java.util.Objects类的equals静态方法取用结果
return age == person.age && Objects.equals(name, person.name);
}
}
```
这段代码充分考虑了对象为空、类型一致等问题但方法内容并不唯一。大多数IDE都可以自动生成equals方法的代码内容。在IntelliJ IDEA中可以使用`Code`菜单中的`Generate…`选项,也可以使用快捷键`alt+insert`,并选择`equals() and hashCode()`进行自动代码生成。如下图所示:
![](img\equals方法1.png)
![](img\equals方法2.png)
![](img\equals方法3.png)
> tipsObject类当中的hashCode等其他方法今后学习。
### 1.4 Objects类
在刚才IDEA自动重写equals代码中使用到了`java.util.Objects`类,那么这个类是什么呢?
在**JDK7**添加了一个Objects工具类它提供了一些方法来操作对象它由一些静态的实用方法组成这些方法是null-save空指针安全的或null-tolerant容忍空指针的用于计算对象的hashcode、返回对象的字符串表示形式、比较两个对象。
在比较两个对象的时候Object的equals方法容易抛出空指针异常而Objects类中的equals方法就优化了这个问题。方法如下
* `public static boolean equals(Object a, Object b)`:判断两个对象是否相等。
我们可以查看一下源码,学习一下:
~~~java
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
~~~
## 第二章 日期时间类
### 2.1 Date类
#### 概述
` java.util.Date`类 表示特定的瞬间,精确到毫秒。
继续查阅Date类的描述发现Date拥有多个构造函数只是部分已经过时但是其中有未过时的构造函数可以把毫秒值转成日期对象。
- `public Date()`分配Date对象并初始化此对象以表示分配它的时间精确到毫秒
- `public Date(long date)`分配Date对象并初始化此对象以表示自从标准基准时间称为“历元epoch即1970年1月1日00:00:00 GMT以来的指定毫秒数。
> tips: 由于我们处于东八区所以我们的基准时间为1970年1月1日8时0分0秒。
简单来说使用无参构造可以自动设置当前系统时间的毫秒时刻指定long类型的构造参数可以自定义毫秒时刻。例如
```java
import java.util.Date;
public class Demo01Date {
public static void main(String[] args) {
// 创建日期对象,把当前的时间
System.out.println(new Date()); // Tue Jan 16 14:37:35 CST 2018
// 创建日期对象,把当前的毫秒值转成日期对象
System.out.println(new Date(0L)); // Thu Jan 01 08:00:00 CST 1970
}
}
```
> tips:在使用println方法时会自动调用Date类中的toString方法。Date类对Object类中的toString方法进行了覆盖重写所以结果为指定格式的字符串。
>
#### 常用方法
Date类中的多数方法已经过时常用的方法有
* `public long getTime()` 把日期对象转换成对应的时间毫秒值。
### 2.2 DateFormat类
`java.text.DateFormat` 是日期/时间格式化子类的抽象类,我们通过这个类可以帮我们完成日期和文本之间的转换,也就是可以在Date对象与String对象之间进行来回转换。
* **格式化**按照指定的格式从Date对象转换为String对象。
* **解析**按照指定的格式从String对象转换为Date对象。
#### 构造方法
由于DateFormat为抽象类不能直接使用所以需要常用的子类`java.text.SimpleDateFormat`。这个类需要一个模式(格式)来指定格式化或解析的标准。构造方法为:
* `public SimpleDateFormat(String pattern)`用给定的模式和默认语言环境的日期格式符号构造SimpleDateFormat。
参数pattern是一个字符串代表日期时间的自定义格式。
#### 格式规则
常用的格式规则为:
| 标识字母(区分大小写) | 含义 |
| ----------- | ---- |
| y | 年 |
| M | 月 |
| d | 日 |
| H | 时 |
| m | 分 |
| s | 秒 |
> 备注更详细的格式规则可以参考SimpleDateFormat类的API文档。
创建SimpleDateFormat对象的代码如
```java
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class Demo02SimpleDateFormat {
public static void main(String[] args) {
// 对应的日期格式如2018-01-16 15:06:38
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}
```
#### 常用方法
DateFormat类的常用方法有
- `public String format(Date date)`将Date对象格式化为字符串。
- `public Date parse(String source)`将字符串解析为Date对象。
##### format方法
使用format方法的代码为
```java
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
把Date对象转换成String
*/
public class Demo03DateFormatMethod {
public static void main(String[] args) {
Date date = new Date();
// 创建日期格式化对象,在获取格式化对象时可以指定风格
DateFormat df = new SimpleDateFormat("yyyy年MM月dd日");
String str = df.format(date);
System.out.println(str); // 2008年1月23日
}
}
```
##### parse方法
使用parse方法的代码为
```java
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/*
把String转换成Date对象
*/
public class Demo04DateFormatMethod {
public static void main(String[] args) throws ParseException {
DateFormat df = new SimpleDateFormat("yyyy年MM月dd日");
String str = "2018年12月11日";
Date date = df.parse(str);
System.out.println(date); // Tue Dec 11 00:00:00 CST 2018
}
}
```
### 2.3 练习
请使用日期时间相关的API计算出一个人已经出生了多少天。
**思路:**
1.获取当前时间对应的毫秒值
2.获取自己出生日期对应的毫秒值
3.两个时间相减(当前时间– 出生日期)
**代码实现:**
```java
public static void function() throws Exception {
System.out.println("请输入出生日期 格式 YYYY-MM-dd");
// 获取出生日期,键盘输入
String birthdayString = new Scanner(System.in).next();
// 将字符串日期,转成Date对象
// 创建SimpleDateFormat对象,写日期模式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 调用方法parse,字符串转成日期对象
Date birthdayDate = sdf.parse(birthdayString);
// 获取今天的日期对象
Date todayDate = new Date();
// 将两个日期转成毫秒值,Date类的方法getTime
long birthdaySecond = birthdayDate.getTime();
long todaySecond = todayDate.getTime();
long secone = todaySecond-birthdaySecond;
if (secone < 0){
System.out.println("还没出生呢");
} else {
System.out.println(secone/1000/60/60/24);
}
}
```
### 2.4 Calendar类
#### 概念
日历我们都见过
![](img\日历.jpg)
`java.util.Calendar`是日历类在Date后出现替换掉了许多Date的方法。该类将所有可能用到的时间信息封装为静态成员变量方便获取。日历类就是方便获取各个时间属性的。
#### 获取方式
Calendar为抽象类由于语言敏感性Calendar类在创建对象时并非直接创建而是通过静态方法创建返回子类对象如下
Calendar静态方法
* `public static Calendar getInstance()`:使用默认时区和语言环境获得一个日历
例如:
```java
import java.util.Calendar;
public class Demo06CalendarInit {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
}
}
```
#### 常用方法
根据Calendar类的API文档常用方法有
- `public abstract void add(int field, int amount)`:根据日历的规则,为给定的日历字段添加或减去指定的时间量。
- `public void set(int field, int value)`:将给定的日历字段设置为给定值。
- `public int get(int field)`:返回给定日历字段的值。
- `public Date getTime()`返回一个表示此Calendar时间值从历元到现在的毫秒偏移量的Date对象。
Calendar类中提供很多成员常量代表给定的日历字段
| 字段值 | 含义 |
| ------------ | -------------------- |
| YEAR | 年 |
| MONTH | 月从0开始可以+1使用 |
| DAY_OF_MONTH | 月中的天(几号) |
| HOUR | 时12小时制 |
| HOUR_OF_DAY | 时24小时制 |
| MINUTE | 分 |
| SECOND | 秒 |
| DAY_OF_WEEK | 周中的天周几周日为1可以-1使用 |
##### get/set方法
get方法用来获取指定字段的值set方法用来设置指定字段的值代码使用演示
```java
import java.util.Calendar;
public class CalendarUtil {
public static void main(String[] args) {
// 创建Calendar对象
Calendar cal = Calendar.getInstance();
// 设置年
int year = cal.get(Calendar.YEAR);
// 设置月
int month = cal.get(Calendar.MONTH) + 1;
// 设置日
int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
System.out.print(year + "年" + month + "月" + dayOfMonth + "日");
}
}
```
```java
import java.util.Calendar;
public class Demo07CalendarMethod {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, 2020);
System.out.print(year + "年" + month + "月" + dayOfMonth + "日"); // 2020年1月17日
}
}
```
##### add方法
add方法可以对指定日历字段的值进行加减操作如果第二个参数为正数则加上偏移量如果为负数则减去偏移量。代码如
```java
import java.util.Calendar;
public class Demo08CalendarMethod {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
System.out.print(year + "年" + month + "月" + dayOfMonth + "日"); // 2018年1月17日
// 使用add方法
cal.add(Calendar.DAY_OF_MONTH, 2); // 加2天
cal.add(Calendar.YEAR, -3); // 减3年
System.out.print(year + "年" + month + "月" + dayOfMonth + "日"); // 2015年1月18日;
}
}
```
##### getTime方法
Calendar中的getTime方法并不是获取毫秒时刻而是拿到对应的Date对象。
```java
import java.util.Calendar;
import java.util.Date;
public class Demo09CalendarMethod {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
Date date = cal.getTime();
System.out.println(date); // Tue Jan 16 16:03:09 CST 2018
}
}
```
> 小贴士:
>
> 西方星期的开始为周日,中国为周一。
>
> 在Calendar类中月份的表示是以0-11代表1-12月。
>
> 日期是有大小关系的,时间靠后,时间越大。
>
## 第三章 System类
`java.lang.System`类中提供了大量的静态方法可以获取与系统相关的信息或系统级操作在System类的API文档中常用的方法有
- `public static long currentTimeMillis()`:返回以毫秒为单位的当前时间。
- `public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)`:将数组中指定的数据拷贝到另一个数组中。
### 3.1 currentTimeMillis方法
实际上currentTimeMillis方法就是 获取当前系统时间与1970年01月01日00:00点之间的毫秒差值
```java
import java.util.Date;
public class SystemDemo {
public static void main(String[] args) {
//获取当前时间毫秒值
System.out.println(System.currentTimeMillis()); // 1516090531144
}
}
```
#### 练习
验证for循环打印数字1-9999所需要使用的时间毫秒
~~~java
public class SystemTest1 {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
long end = System.currentTimeMillis();
System.out.println("共耗时毫秒" + (end - start));
}
}
~~~
### 3.2 arraycopy方法
* `public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)`将数组中指定的数据拷贝到另一个数组中
数组的拷贝动作是系统级的性能很高System.arraycopy方法具有5个参数含义分别为
| 参数序号 | 参数名称 | 参数类型 | 参数含义 |
| ---- | ------- | ------ | ---------- |
| 1 | src | Object | 源数组 |
| 2 | srcPos | int | 源数组索引起始位置 |
| 3 | dest | Object | 目标数组 |
| 4 | destPos | int | 目标数组索引起始位置 |
| 5 | length | int | 复制元素个数 |
#### 练习
将src数组中前3个元素复制到dest数组的前3个位置上复制元素前src数组元素[1,2,3,4,5]dest数组元素[6,7,8,9,10]复制元素后src数组元素[1,2,3,4,5]dest数组元素[1,2,3,9,10]
```java
import java.util.Arrays;
public class Demo11SystemArrayCopy {
public static void main(String[] args) {
int[] src = new int[]{1,2,3,4,5};
int[] dest = new int[]{6,7,8,9,10};
System.arraycopy( src, 0, dest, 0, 3);
/*代码运行后:两个数组中的元素发生了变化
src数组元素[1,2,3,4,5]
dest数组元素[1,2,3,9,10]
*/
}
}
```
## 第四章 包装类
### 4.1 概述
Java提供了两个类型系统基本类型与引用类型使用基本类型在于效率然而很多情况会创建对象使用因为对象可以做更多的功能如果想要我们的基本类型像对象一样操作就可以使用基本类型对应的包装类如下
| 基本类型 | 对应的包装类位于java.lang包中 |
| ------- | --------------------- |
| byte | Byte |
| short | Short |
| int | **Integer** |
| long | Long |
| float | Float |
| double | Double |
| char | **Character** |
| boolean | Boolean |
### 4.2 装箱与拆箱
基本类型与对应的包装类对象之间来回转换的过程称为装箱拆箱“:
* **装箱**从基本类型转换为对应的包装类对象
* **拆箱**从包装类对象转换为对应的基本类型
用Integer与 int为例看懂代码即可
基本数值---->包装对象
~~~java
Integer i = new Integer(4);//使用构造函数函数
Integer iii = Integer.valueOf(4);//使用包装类中的valueOf方法
~~~
包装对象---->基本数值
~~~java
int num = i.intValue();
~~~
### 4.3自动装箱与自动拆箱
由于我们经常要做基本类型与包装类之间的转换从Java 5JDK 1.5)开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:
```java
Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。
```
### 4.4 基本类型与字符串之间的转换
#### 基本类型转换为String
基本类型转换String总共有三种方式查看课后资料可以得知这里只讲最简单的一种方式
~~~
基本类型直接与””相连接即可34+""
~~~
String转换成对应的基本类型
除了Character类之外其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型
- `public static byte parseByte(String s)`将字符串参数转换为对应的byte基本类型。
- `public static short parseShort(String s)`将字符串参数转换为对应的short基本类型。
- `public static int parseInt(String s)`将字符串参数转换为对应的int基本类型。
- `public static long parseLong(String s)`将字符串参数转换为对应的long基本类型。
- `public static float parseFloat(String s)`将字符串参数转换为对应的float基本类型。
- `public static double parseDouble(String s)`将字符串参数转换为对应的double基本类型。
- `public static boolean parseBoolean(String s)`将字符串参数转换为对应的boolean基本类型。
代码使用仅以Integer类的静态方法parseXxx为例
```java
public class Demo18WrapperParse {
public static void main(String[] args) {
int num = Integer.parseInt("100");
}
}
```
> 注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出`java.lang.NumberFormatException`异常。
>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -0,0 +1,664 @@
# day02【Collection、泛型】
### 主要内容
- Collection集合
- 迭代器
- 增强for
- 泛型
### 教学目标
- [ ] 能够说出集合与数组的区别
- [ ] 说出Collection集合的常用功能
- [ ] 能够使用迭代器对集合进行取元素
- [ ] 能够说出集合的使用细节
- [ ] 能够使用集合存储自定义类型
- [ ] 能够使用foreach循环遍历集合
- [ ] 能够使用泛型定义集合对象
- [ ] 能够理解泛型上下限
- [ ] 能够阐述泛型通配符的作用
## 第一章 Collection集合
### 1.1 集合概述
在前面基础班我们已经学习过并使用过集合ArrayList\<E> ,那么集合到底是什么呢?
* **集合**集合是java中提供的一种容器可以用来存储多个数据。
集合和数组既然都是容器,它们有啥区别呢?
* 数组的长度是固定的。集合的长度是可变的。
* 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
### 1.2 集合框架
JAVASE提供了满足各种需求的API在使用这些API前先了解其继承与接口操作架构才能了解何时采用哪个类以及类之间如何彼此合作从而达到灵活应用。
集合按照其存储结构可以分为两大类,分别是单列集合`java.util.Collection`和双列集合`java.util.Map`,今天我们主要学习`Collection`集合在day04时讲解`Map`集合。
* **Collection**:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是`java.util.List``java.util.Set`。其中,`List`的特点是元素有序、元素可重复。`Set`的特点是元素无序,而且不可重复。`List`接口的主要实现类有`java.util.ArrayList``java.util.LinkedList``Set`接口的主要实现类有`java.util.HashSet``java.util.TreeSet`
从上面的描述可以看出JDK中提供了丰富的集合类库为了便于初学者进行系统地学习接下来通过一张图来描述整个集合类的继承体系。
![](img\Collection集合体系图.png)
其中,橙色框里填写的都是接口类型,而蓝色框里填写的都是具体的实现类。这几天将针对图中所列举的集合类进行逐一地讲解。
集合本身是一个工具它存放在java.util包中。在`Collection`接口定义着单列集合框架中最最共性的内容。
### 1.3 Collection 常用功能
Collection是所有单列集合的父接口因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:
* `public boolean add(E e)` 把给定的对象添加到当前集合中 。
* `public void clear()` :清空集合中所有的元素。
* `public boolean remove(E e)`: 把给定的对象在当前集合中删除。
* `public boolean contains(E e)`: 判断当前集合中是否包含给定的对象。
* `public boolean isEmpty()`: 判断当前集合是否为空。
* `public int size()`: 返回集合中元素的个数。
* `public Object[] toArray()`: 把集合中的元素,存储到数组中。
方法演示:
~~~java
import java.util.ArrayList;
import java.util.Collection;
public class Demo1Collection {
public static void main(String[] args) {
// 创建集合对象
// 使用多态形式
Collection<String> coll = new ArrayList<String>();
// 使用方法
// 添加功能 boolean add(String s)
coll.add("小李广");
coll.add("扫地僧");
coll.add("石破天");
System.out.println(coll);
// boolean contains(E e) 判断o是否在集合中存在
System.out.println("判断 扫地僧 是否在集合中"+coll.contains("扫地僧"));
//boolean remove(E e) 删除在集合中的o元素
System.out.println("删除石破天:"+coll.remove("石破天"));
System.out.println("操作之后集合中元素:"+coll);
// size() 集合中有几个元素
System.out.println("集合中有"+coll.size()+"个元素");
// Object[] toArray()转换成一个Object数组
Object[] objects = coll.toArray();
// 遍历数组
for (int i = 0; i < objects.length; i++) {
System.out.println(objects[i]);
}
// void clear() 清空集合
coll.clear();
System.out.println("集合中内容为"+coll);
// boolean isEmpty() 判断是否为空
System.out.println(coll.isEmpty());
}
}
~~~
> tips: 有关Collection中的方法可不止上面这些其他方法可以自行查看API学习。
## 第二章 Iterator迭代器
### 2.1 Iterator接口
在程序开发中经常需要遍历集合中的所有元素针对这种需求JDK专门提供了一个接口`java.util.Iterator``Iterator`接口也是Java集合中的一员但它与`Collection``Map`接口有所不同`Collection`接口与`Map`接口主要用于存储元素`Iterator`主要用于迭代访问即遍历`Collection`中的元素因此`Iterator`对象也被称为迭代器
想要遍历Collection集合那么就要获取该集合迭代器完成迭代操作下面介绍一下获取迭代器的方法
* `public Iterator iterator()`: 获取集合对应的迭代器用来遍历集合中的元素的
下面介绍一下迭代的概念
* **迭代**即Collection集合元素的通用获取方式在取元素之前先要判断集合中有没有元素如果有就把这个元素取出来继续在判断如果还有就再取出出来一直把集合中的所有元素全部取出这种取出方式专业术语称为迭代
Iterator接口的常用方法如下
* `public E next()`:返回迭代的下一个元素
* `public boolean hasNext()`:如果仍有元素可以迭代则返回 true
接下来我们通过案例学习如何使用Iterator迭代集合中元素
~~~java
public class IteratorDemo {
public static void main(String[] args) {
// 使用多态方式 创建对象
Collection<String> coll = new ArrayList<String>();
// 添加元素到集合
coll.add("串串星人");
coll.add("吐槽星人");
coll.add("汪星人");
//遍历
//使用迭代器 遍历 每个集合对象都有自己的迭代器
Iterator<String> it = coll.iterator();
// 泛型指的是 迭代出 元素的数据类型
while(it.hasNext()){ //判断是否有迭代元素
String s = it.next();//获取迭代出的元素
System.out.println(s);
}
}
}
~~~
> tips:在进行集合元素取出时如果集合中已经没有元素了还继续使用迭代器的next方法将会发生java.util.NoSuchElementException没有集合元素的错误。
### 2.2 迭代器的实现原理
我们在之前案例已经完成了Iterator遍历集合的整个过程。当遍历集合时首先通过调用t集合的iterator()方法获得迭代器对象然后使用hashNext()方法判断集合中是否存在下一个元素如果存在则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。
Iterator迭代器对象在遍历集合时内部采用指针的方式来跟踪集合中的元素为了让初学者能更好地理解迭代器的工作原理接下来通过一个图例来演示Iterator对象迭代元素的过程
![](img\迭代器原理图.bmp)
在调用Iterator的next方法之前迭代器的索引位于第一个元素之前不指向任何元素当第一次调用迭代器的next方法后迭代器的索引会向后移动一位指向第一个元素并将该元素返回当再次调用next方法时迭代器的索引会指向第二个元素并将该元素返回依此类推直到hasNext方法返回false表示到达了集合的末尾终止对元素的遍历。
### 2.3 增强for
增强for循环(也称for each循环)是**JDK1.5**以后出来的一个高级for循环专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器所以在遍历的过程中不能对集合中的元素进行增删操作。
格式:
~~~java
for(元素的数据类型 变量 : Collection集合or数组){
//写操作代码
}
~~~
它用于遍历Collection和数组。通常只进行遍历元素不要在遍历的过程中对集合元素进行增删操作。
##### 练习1遍历数组
~~~java
public class NBForDemo1 {
public static void main(String[] args) {
int[] arr = {3,5,6,87};
//使用增强for遍历数组
for(int a : arr){//a代表数组中的每个元素
System.out.println(a);
}
Collection<String> coll = new ArrayList<String>();
coll.add("小河神");
coll.add("老河神");
coll.add("神婆");
for(String s :coll){
System.out.println(s);
}
}
}
~~~
##### 练习2:遍历集合
~~~java
public class NBFor {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<String>();
coll.add("小河神");
coll.add("老河神");
coll.add("神婆");
//使用增强for遍历
for(String s :coll){//接收变量s代表 代表被遍历到的集合元素
System.out.println(s);
}
}
}
~~~
> tips: 新for循环必须有被遍历的目标。目标只能是Collection或者是数组。新式for仅仅作为遍历操作出现。
## 第三章 泛型
### 3.1 泛型概述
在前面学习集合时我们都知道集合中是可以存放任意对象的只要把对象存储集合后那么这时他们都会被提升成Object类型。当我们在取出每一个对象并且进行相应的操作这时必须采用类型转换。
大家观察下面代码:
~~~java
public class GenericDemo {
public static void main(String[] args) {
Collection coll = new ArrayList();
coll.add("abc");
coll.add("itcast");
coll.add(5);//由于集合没有做任何限定,任何类型都可以给其中存放
Iterator it = coll.iterator();
while(it.hasNext()){
//需要打印每个字符串的长度,就要把迭代出来的对象转成String类型
String str = (String) it.next();
System.out.println(str.length());
}
}
}
~~~
程序在运行时发生了问题**java.lang.ClassCastException**。为什么会发生类型转换异常呢? 我们来分析下:由于集合中什么类型的元素都可以存储。导致取出时强转引发运行时 ClassCastException。 怎么来解决这个问题呢? Collection虽然可以存储各种对象但实际上通常Collection只存储同一类型对象。例如都是存储字符串对象。因此在JDK5之后新增了**泛型**(**Generic**)语法让你在设计API时可以指定类或方法支持泛型这样我们使用API的时候也变得更为简洁并得到了编译时期的语法检查。
* **泛型**:可以在类或方法中预支地使用未知的类型。
> tips:一般在创建对象时将未知的类型确定具体的类型。当没有指定泛型时默认类型为Object类型。
### 3.2 使用泛型的好处
上一节只是讲解了泛型的引入,那么泛型带来了哪些好处呢?
* 将运行时期的ClassCastException转移到了编译时期变成了编译失败。
* 避免了类型强转的麻烦。
通过我们如下代码体验一下:
~~~java
public class GenericDemo2 {
public static void main(String[] args) {
Collection<String> list = new ArrayList<String>();
list.add("abc");
list.add("itcast");
// list.add(5);//当集合明确类型后,存放类型不一致就会编译报错
// 集合已经明确具体存放的元素类型,那么在使用迭代器的时候,迭代器也同样会知道具体遍历元素类型
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
//当使用Iterator<String>控制元素类型后就不需要强转了。获取到的元素直接就是String类型
System.out.println(str.length());
}
}
}
~~~
> tips:泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。
### 3.3 泛型的定义与使用
我们在集合中会大量使用到泛型,这里来完整地学习泛型知识。
泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递。
#### 定义和使用含有泛型的类
定义格式:
~~~
修饰符 class 类名\<代表泛型的变量> { }
~~~
例如API中的ArrayList集合
~~~java
class ArrayList<E>{
public boolean add(E e){ }
public E get(int index){ }
....
}
~~~
使用泛型: 即什么时候确定泛型。
**在创建对象的时候确定泛型**
例如,`ArrayList<String> list = new ArrayList<String>();`
此时变量E的值就是String类型,那么我们的类型就可以理解为:
~~~java
class ArrayList<String>{
public boolean add(String e){ }
public String get(int index){ }
...
}
~~~
再例如,`ArrayList<Integer> list = new ArrayList<Integer>();`
此时变量E的值就是Integer类型,那么我们的类型就可以理解为:
~~~java
class ArrayList<Integer> {
public boolean add(Integer e) { }
public Integer get(int index) { }
...
}
~~~
举例自定义泛型类
~~~java
public class MyGenericClass<MVP> {
//没有MVP类型在这里代表 未知的一种数据类型 未来传递什么就是什么类型
private MVP mvp;
public void setMVP(MVP mvp) {
this.mvp = mvp;
}
public MVP getMVP() {
return mvp;
}
}
~~~
使用:
~~~java
public class GenericClassDemo {
public static void main(String[] args) {
// 创建一个泛型为String的类
MyGenericClass<String> my = new MyGenericClass<String>();
// 调用setMVP
my.setMVP("大胡子登登");
// 调用getMVP
String mvp = my.getMVP();
System.out.println(mvp);
//创建一个泛型为Integer的类
MyGenericClass<Integer> my2 = new MyGenericClass<Integer>();
my2.setMVP(123);
Integer mvp2 = my2.getMVP();
}
}
~~~
#### 含有泛型的方法
定义格式:
~~~
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
~~~
例如,
~~~java
public class MyGenericMethod {
public <MVP> void show(MVP mvp) {
System.out.println(mvp.getClass());
}
public <MVP> MVP show2(MVP mvp) {
return mvp;
}
}
~~~
使用格式:**调用方法时,确定泛型的类型**
~~~java
public class GenericMethodDemo {
public static void main(String[] args) {
// 创建对象
MyGenericMethod mm = new MyGenericMethod();
// 演示看方法提示
mm.show("aaa");
mm.show(123);
mm.show(12.45);
}
}
~~~
#### 含有泛型的接口
定义格式:
~~~
修饰符 interface接口名<代表泛型的变量> { }
~~~
例如,
~~~java
public interface MyGenericInterface<E>{
public abstract void add(E e);
public abstract E getE();
}
~~~
使用格式:
**1、定义类时确定泛型的类型**
例如
~~~java
public class MyImp1 implements MyGenericInterface<String> {
@Override
public void add(String e) {
// 省略...
}
@Override
public String getE() {
return null;
}
}
~~~
此时泛型E的值就是String类型。
**2、始终不确定泛型的类型直到创建对象时确定泛型的类型**
例如
~~~java
public class MyImp2<E> implements MyGenericInterface<E> {
@Override
public void add(E e) {
// 省略...
}
@Override
public E getE() {
return null;
}
}
~~~
确定泛型:
~~~java
/*
* 使用
*/
public class GenericInterface {
public static void main(String[] args) {
MyImp2<String> my = new MyImp2<String>();
my.add("aa");
}
}
~~~
### 3.4 泛型通配符
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符\<?>表示。但是一旦使用泛型的通配符后只能使用Object类中的共性方法集合中元素自身方法无法使用。
##### 通配符基本使用
泛型的通配符:**不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。**
此时只能接受数据,不能往该集合中存储数据。
举个例子大家理解使用即可:
~~~java
public static void main(String[] args) {
Collection<Intger> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
}
public static void getElement(Collection<?> coll){}
//?代表可以接收任意类型
~~~
> tips:泛型不存在继承关系 Collection\<Object> list = new ArrayList\<String>();这种是错误的。
##### 通配符高级使用----受限泛型
之前设置泛型的时候实际上是可以任意设置的只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的**上限**和**下限**。
**泛型的上限**
* **格式** `类型名称 <? extends 类 > 对象名称`
* **意义** `只能接收该类型及其子类`
**泛型的下限**
- **格式** `类型名称 <? super 类 > 对象名称`
- **意义** `只能接收该类型及其父类型`
比如现已知Object类String 类Number类Integer类其中Number是Integer的父类
~~~java
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();
getElement(list1);
getElement(list2);//报错
getElement(list3);
getElement(list4);//报错
getElement2(list1);//报错
getElement2(list2);//报错
getElement2(list3);
getElement2(list4);
}
// 泛型的上限:此时的泛型?必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此时的泛型?必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}
~~~
## 第四章 集合综合案例
### 4.1 案例介绍
按照斗地主的规则,完成洗牌发牌的动作。
具体规则:
使用54张牌打乱顺序,三个玩家参与游戏三人交替摸牌每人17张牌最后三张留作底牌。
### 4.2 案例分析
* 准备牌:
牌可以设计为一个ArrayList\<String>,每个字符串为一张牌。
每张牌由花色数字两部分组成,我们可以使用花色集合与数字集合嵌套迭代完成每张牌的组装。
牌由Collections类的shuffle方法进行随机排序。
* 发牌
将每个人以及底牌设计为ArrayList\<String>,将最后3张牌直接存放于底牌剩余牌通过对3取模依次发牌。
* 看牌
直接打印每个集合。
### 4.3 代码实现
~~~java
import java.util.ArrayList;
import java.util.Collections;
public class Poker {
public static void main(String[] args) {
/*
* 1: 准备牌操作
*/
//1.1 创建牌盒 将来存储牌面的
ArrayList<String> pokerBox = new ArrayList<String>();
//1.2 创建花色集合
ArrayList<String> colors = new ArrayList<String>();
//1.3 创建数字集合
ArrayList<String> numbers = new ArrayList<String>();
//1.4 分别给花色 以及 数字集合添加元素
colors.add("♥");
colors.add("♦");
colors.add("♠");
colors.add("♣");
for(int i = 2;i<=10;i++){
numbers.add(i+"");
}
numbers.add("J");
numbers.add("Q");
numbers.add("K");
numbers.add("A");
//1.5 创造牌 拼接牌操作
// 拿出每一个花色 然后跟每一个数字 进行结合 存储到牌盒中
for (String color : colors) {
//color每一个花色
//遍历数字集合
for(String number : numbers){
//结合
String card = color+number;
//存储到牌盒中
pokerBox.add(card);
}
}
//1.6大王小王
pokerBox.add("小☺");
pokerBox.add("大☠");
// System.out.println(pokerBox);
//洗牌 是不是就是将 牌盒中 牌的索引打乱
// Collections类 工具类 都是 静态方法
// shuffer方法
/*
* static void shuffle(List<?> list)
* 使用默认随机源对指定列表进行置换。
*/
//2:洗牌
Collections.shuffle(pokerBox);
//3 发牌
//3.1 创建 三个 玩家集合 创建一个底牌集合
ArrayList<String> player1 = new ArrayList<String>();
ArrayList<String> player2 = new ArrayList<String>();
ArrayList<String> player3 = new ArrayList<String>();
ArrayList<String> dipai = new ArrayList<String>();
//遍历 牌盒 必须知道索引
for(int i = 0;i<pokerBox.size();i++){
//获取 牌面
String card = pokerBox.get(i);
//留出三张底牌 存到 底牌集合中
if(i>=51){//存到底牌集合中
dipai.add(card);
} else {
//玩家1 %3 ==0
if(i%3==0){
player1.add(card);
}else if(i%3==1){//玩家2
player2.add(card);
}else{//玩家3
player3.add(card);
}
}
}
//看看
System.out.println("令狐冲:"+player1);
System.out.println("田伯光:"+player2);
System.out.println("绿竹翁:"+player3);
System.out.println("底牌:"+dipai);
}
}
~~~

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

View File

@@ -0,0 +1,712 @@
# day03 【List、Set、数据结构、Collections】
### 主要内容
- List集合
- Set集合
- 数据结构
### 教学目标
- [ ] 能够说出List集合特点
- [ ] 能够说出常见的数据结构
- [ ] 能够说出数组结构特点
- [ ] 能够说出栈结构特点
- [ ] 能够说出队列结构特点
- [ ] 能够说出单向链表结构特点
- [ ] 能够说出Set集合的特点
- [ ] 能够说出哈希表的特点
- [ ] 使用HashSet集合存储自定义元素
- [ ] 能够说出可变参数的格式
- [ ] 能够使用集合工具类
- [ ] 能够使用Comparator比较器进行排序
## 第一章 List集合
我们掌握了Collection接口的使用后再来看看Collection接口中的子类他们都具备那些特性呢
接下来我们一起学习Collection中的常用几个子类`java.util.List`集合、`java.util.Set`集合)。
### 1.1 List接口介绍
`java.util.List`接口继承自`Collection`接口,是单列集合的一个重要分支,习惯性地会将实现了`List`接口的对象称为List集合。在List集合中允许出现重复的元素所有的元素是以一种线性方式进行存储的在程序中可以通过索引来访问集合中的指定元素。另外List集合还有一个特点就是元素有序即元素的存入顺序和取出顺序一致。
看完API我们总结一下
List接口特点
1. 它是一个元素存取有序的集合。例如存元素的顺序是11、22、33。那么集合中元素的存储就是按照11、22、33的顺序完成的
2. 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
3. 集合中可以有重复的元素通过元素的equals方法来比较是否为重复的元素。
> tips:我们在基础班的时候已经学习过List接口的子类java.util.ArrayList类该类中的方法都是来自List中定义。
### 1.2 List接口中常用方法
List作为Collection集合的子接口不但继承了Collection接口中的全部方法而且还增加了一些根据元素索引来操作集合的特有方法如下
* `public void add(int index, E element)`: 将指定的元素,添加到该集合中的指定位置上。
* `public E get(int index)`:返回集合中指定位置的元素。
* `public E remove(int index)`: 移除列表中指定位置的元素, 返回的是被移除的元素。
* `public E set(int index, E element)`:用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
List集合特有的方法都是跟索引相关我们在基础班都学习过那么我们再来复习一遍吧
~~~java
public class ListDemo {
public static void main(String[] args) {
// 创建List集合对象
List<String> list = new ArrayList<String>();
// 往 尾部添加 指定元素
list.add("图图");
list.add("小美");
list.add("不高兴");
System.out.println(list);
// add(int index,String s) 往指定位置添加
list.add(1,"没头脑");
System.out.println(list);
// String remove(int index) 删除指定位置元素 返回被删除元素
// 删除索引位置为2的元素
System.out.println("删除索引位置为2的元素");
System.out.println(list.remove(2));
System.out.println(list);
// String set(int index,String s)
// 在指定位置 进行 元素替代(改)
// 修改指定位置元素
list.set(0, "三毛");
System.out.println(list);
// String get(int index) 获取指定位置元素
// 跟size() 方法一起用 来 遍历的
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
//还可以使用增强for
for (String string : list) {
System.out.println(string);
}
}
}
~~~
> tips:我们之前学习Colletion体系的时候发现List集合下有很多集合它们的存储结构不同这样就导致了这些集合它们有各自的特点供我们在不同的环境下使用那么常见的数据结构有哪些呢在下一章我们来介绍
## 第二章 数据结构
### 2.1 数据结构有什么用?
当你用着java里面的容器类很爽的时候你有没有想过怎么ArrayList就像一个无限扩充的数组也好像链表之类的好用吗好用这就是数据结构的用处只不过你在不知不觉中使用了
现实世界的存储我们使用的工具和建模每种数据结构有自己的优点和缺点想想如果Google的数据用的是数组的存储我们还能方便地查询到所需要的数据吗而算法在这么多的数据中如何做到最快的插入查找删除也是在追求更快
我们java是面向对象的语言就好似自动档轿车C语言好似手动档吉普数据结构呢是变速箱的工作原理你完全可以不知道变速箱怎样工作就把自动档的车子从 A点 开到 B点而且未必就比懂得的人慢写程序这件事和开车一样经验可以起到很大作用但如果你不知道底层是怎么工作的就永远只能开车既不会修车也不能造车当然了数据结构内容比较多细细的学起来也是相对费功夫的不可能达到一蹴而就我们将常见的数据结构堆栈队列数组链表和红黑树 这几种给大家介绍一下作为数据结构的入门了解一下它们的特点即可
![](img\数据结构比喻.png)
### 2.2 常见的数据结构
数据存储的常用结构有队列数组链表和红黑树我们分别来了解一下
##### 栈
* ******stack**,又称堆栈它是运算受限的线性表其限制是仅允许在标的一端进行插入和删除操作不允许在其他任何位置进行添加查找删除等操作
简单的说采用该结构的集合对元素的存取有如下的特点
* 先进后出存进去的元素要在后它后面的元素依次取出后才能取出该元素)。例如子弹压进弹夹先压进去的子弹在下面后压进去的子弹在上面当开枪时先弹出上面的子弹然后才能弹出下面的子弹
* 栈的入口出口的都是栈的顶端位置
![](img\堆.png)
这里两个名词需要注意
* **压栈**就是存元素把元素存储到栈的顶端位置栈中已有元素依次向栈底方向移动一个位置
* **弹栈**就是取元素把栈的顶端位置元素取出栈中已有元素依次向栈顶方向移动一个位置
##### 队列
* **队列****queue**,简称队它同堆栈一样也是一种运算受限的线性表其限制是仅允许在表的一端进行插入而在表的另一端进行删除
简单的说采用该结构的集合对元素的存取有如下的特点
* 先进先出存进去的元素要在后它前面的元素依次取出后才能取出该元素)。例如小火车过山洞车头先进去车尾后进去车头先出来车尾后出来
* 队列的入口出口各占一侧例如下图中的左侧为入口右侧为出口
![](img\队列图.bmp)
##### 数组
* **数组**:**Array**,是有序的元素序列数组是在内存中开辟一段连续的空间并在此空间存放元素就像是一排出租屋有100个房间从001到100每个房间都有固定编号通过编号就可以快速找到租房子的人
简单的说,采用该结构的集合对元素的存取有如下的特点
* 查找元素快通过索引可以快速访问指定位置的元素
![](img/数组查询快.png)
* 增删元素慢
* **指定索引位置增加元素**需要创建一个新数组将指定新元素存储在指定索引位置再把原数组元素根据索引复制到新数组对应索引的位置如下图![](img/数组添加.png)
* **指定索引位置删除元素**需要创建一个新数组把原数组元素根据索引复制到新数组对应索引的位置原数组中指定索引位置元素不复制到新数组中如下图![](img/数组删除.png)
##### 链表
* **链表**:**linked list**,由一系列结点node链表中每一个元素称为结点组成结点可以在运行时i动态生成每个结点包括两个部分一个是存储数据元素的数据域另一个是存储下一个结点地址的指针域我们常说的链表结构有单向链表与双向链表那么这里给大家介绍的是**单向链表**。
![](img\单链表结构特点.png)
简单的说采用该结构的集合对元素的存取有如下的特点
* 多个结点之间通过地址进行连接例如多个人手拉手每个人使用自己的右手拉住下个人的左手依次类推这样多个人就连在一起了
![](img\单链表结构.png)
* 查找元素慢想查找某个元素需要通过连接的节点依次向后查找指定元素
* 增删元素快
* 增加元素只需要修改连接下个元素的地址即可
![](img\增加结点.png)
* 删除元素只需要修改连接下个元素的地址即可
![](img\删除结点.bmp)
##### 红黑树
* **二叉树****binary tree** ,是每个结点不超过2的有序**tree**
简单的理解就是一种类似于我们生活中树的结构只不过每个结点上都最多只能有两个子结点
二叉树是每个节点最多有两个子树的树结构顶上的叫根结点两边被称作左子树右子树”。
如图
![](img\二叉树.bmp)
我们要说的是二叉树的一种比较有意思的叫做**红黑树**红黑树本身就是一颗二叉查找树将节点插入后该树仍然是一颗二叉查找树也就意味着树的键值仍然是有序的
## 第三章 List的子类
### 3.1 ArrayList集合
`java.util.ArrayList`集合数据存储的结构是数组结构元素增删慢查找快由于日常开发中使用最多的功能为查询数据遍历数据所以`ArrayList`是最常用的集合
许多程序员开发时非常随意地使用ArrayList完成任何需求并不严谨这种用法是不提倡的
### 3.2 LinkedList集合
`java.util.LinkedList`集合数据存储的结构是链表结构方便元素添加删除的集合
> LinkedList是一个双向链表那么双向链表是什么样子的呢我们用个图了解下
![](img\双向链表.png)
实际开发中对一个集合元素的添加与删除经常涉及到首尾操作而LinkedList提供了大量首尾操作的方法这些方法我们作为了解即可
* `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
LinkedList是List的子类List中的方法LinkedList都是可以使用这里就不做详细介绍我们只需要了解LinkedList的特有方法即可在开发时LinkedList集合也可以作为堆栈队列的结构使用。(了解即可
方法演示
~~~java
public class LinkedListDemo {
public static void main(String[] args) {
LinkedList<String> link = new LinkedList<String>();
//添加元素
link.addFirst("abc1");
link.addFirst("abc2");
link.addFirst("abc3");
System.out.println(link);
// 获取元素
System.out.println(link.getFirst());
System.out.println(link.getLast());
// 删除元素
System.out.println(link.removeFirst());
System.out.println(link.removeLast());
while (!link.isEmpty()) { //判断集合是否为空
System.out.println(link.pop()); //弹出集合中的栈顶元素
}
System.out.println(link);
}
}
~~~
## 第四章 Set接口
`java.util.Set`接口和`java.util.List`接口一样,同样继承自`Collection`接口,它与`Collection`接口中的方法基本一致,并没有对`Collection`接口进行功能上的扩充,只是比`Collection`接口更加严格了。与`List`接口不同的是,`Set`接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
`Set`集合有多个子类,这里我们介绍其中的`java.util.HashSet``java.util.LinkedHashSet`这两个集合。
> tips:Set集合取出元素的方式可以采用迭代器、增强for。
### 4.1 HashSet集合介绍
`java.util.HashSet``Set`接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。`java.util.HashSet`底层的实现其实是一个`java.util.HashMap`支持,由于我们暂时还未学习,先做了解。
`HashSet`是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:`hashCode``equals`方法。
我们先来使用一下Set集合存储看下现象再进行原理的讲解:
~~~java
public class HashSetDemo {
public static void main(String[] args) {
//创建 Set集合
HashSet<String> set = new HashSet<String>();
//添加元素
set.add(new String("cba"));
set.add("abc");
set.add("bac");
set.add("cba");
//遍历
for (String name : set) {
System.out.println(name);
}
}
}
~~~
输出结果如下,说明集合中不能存储重复元素:
~~~
cba
abc
bac
~~~
> tips:根据结果我们发现字符串"cba"只存储了一个也就是说重复的元素set集合不存储。
### 4.2 HashSet集合存储数据的结构哈希表
什么是哈希表呢?
在**JDK1.8**之前,哈希表底层采用数组+链表实现即使用链表处理冲突同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多即hash值相等的元素较多时通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现当链表长度超过阈值8将链表转换为红黑树这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树JDK1.8增加了红黑树部分)实现的,如下图所示。![](img\哈希表.png)
看到这张图就有人要问了,这个是怎么存储的呢?
为了方便大家的理解我们结合一个存储流程图来说明一下:
![](img\哈希流程图.png)
总而言之,**JDK1.8**引入红黑树大程度优化了HashMap的性能那么对于我们来讲保证HashSet集合元素的唯一其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象那么保证其唯一就必须复写hashCode和equals方法建立属于当前对象的比较方式。
### 4.3 HashSet存储自定义类型元素
给HashSet中存放自定义类型元素时需要重写对象中的hashCode和equals方法建立自己的比较方式才能保证HashSet集合中的对象唯一
创建自定义Student类
~~~java
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
~~~
~~~java
public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象 该集合中存储 Student类型对象
HashSet<Student> stuSet = new HashSet<Student>();
//存储
Student stu = new Student("于谦", 43);
stuSet.add(stu);
stuSet.add(new Student("郭德纲", 44));
stuSet.add(new Student("于谦", 43));
stuSet.add(new Student("郭麒麟", 23));
stuSet.add(stu);
for (Student stu2 : stuSet) {
System.out.println(stu2);
}
}
}
执行结果:
Student [name=郭德纲, age=44]
Student [name=于谦, age=43]
Student [name=郭麒麟, age=23]
~~~
### 4.4 LinkedHashSet
我们知道HashSet保证元素唯一可是元素存放进去是没有顺序的那么我们要保证有序怎么办呢
在HashSet下面有一个子类`java.util.LinkedHashSet`,它是链表和哈希表组合的一个数据存储结构。
演示代码如下:
~~~java
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
结果:
bbb
aaa
abc
bbc
~~~
### 4.5 可变参数
在**JDK1.5**之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化成如下格式:
```
修饰符 返回值类型 方法名(参数类型... 形参名){ }
```
其实这个书写完全等价与
```
修饰符 返回值类型 方法名(参数类型[] 形参名){ }
```
只是后面这种定义,在调用时必须传递数组,而前者可以直接传递数据即可。
**JDK1.5**以后。出现了简化操作。**...** 用在参数上,称之为可变参数。
同样是代表数组,但是在调用这个带有可变参数的方法时,不用创建数组(这就是简单之处)直接将数组中的元素作为实际参数进行传递其实编译成的class文件将这些元素先封装到一个数组中在进行传递。这些动作都在编译.class文件时自动完成了。
代码演示:
```java
public class ChangeArgs {
public static void main(String[] args) {
int[] arr = { 1, 4, 62, 431, 2 };
int sum = getSum(arr);
System.out.println(sum);
// 6 7 2 12 2121
// 求 这几个元素和 6 7 2 12 2121
int sum2 = getSum(6, 7, 2, 12, 2121);
System.out.println(sum2);
}
/*
* 完成数组 所有元素的求和 原始写法
public static int getSum(int[] arr){
int sum = 0;
for(int a : arr){
sum += a;
}
return sum;
}
*/
//可变参数写法
public static int getSum(int... arr) {
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
}
```
> tips: 上述add方法在同一个类中只能存在一个。因为会发生调用的不确定性
>
> 注意:如果在方法书写时,这个方法拥有多参数,参数中包含可变参数,可变参数一定要写在参数列表的末尾位置。
## 第五章 Collections
### 5.1 常用功能
* `java.utils.Collections`是集合工具类,用来对集合进行操作。部分方法如下:
- `public static <T> boolean addAll(Collection<T> c, T... elements) `:往集合中添加一些元素。
- `public static void shuffle(List<?> list) 打乱顺序`:打乱集合顺序。
- `public static <T> void sort(List<T> list)`:将集合中元素按照默认规则排序。
- `public static <T> void sort(List<T> listComparator<? super T> )`:将集合中元素按照指定规则排序。
代码演示:
```java
public class CollectionsDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
//原来写法
//list.add(12);
//list.add(14);
//list.add(15);
//list.add(1000);
//采用工具类 完成 往集合中添加元素
Collections.addAll(list, 5, 222, 12);
System.out.println(list);
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
结果
[5, 222, 1, 2]
[1, 2, 5, 222]
```
代码演示之后 ,发现我们的集合按照顺序进行了排列,可是这样的顺序是采用默认的顺序,如果想要指定顺序那该怎么办呢?
我们发现还有个方法没有讲,`public static <T> void sort(List<T> listComparator<? super T> )`:将集合中元素按照指定规则排序。接下来讲解一下指定规则的排列。
### 5.2 Comparator比较器
我们还是先研究这个方法
`public static <T> void sort(List<T> list)`:将集合中元素按照默认规则排序。
不过这次存储的是字符串类型。
```java
public class CollectionsDemo2 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
```
结果:
```
[aba, cba, nba, sba]
```
我们使用的是默认的规则完成字符串的排序,那么默认规则是怎么定义出来的呢?
说到排序了简单的说就是两个对象之间比较大小那么在JAVA中提供了两种比较实现的方式一种是比较死板的采用`java.lang.Comparable`接口去实现,一种是灵活的当我需要做排序的时候在去选择的`java.util.Comparator`接口完成。
那么我们采用的`public static <T> void sort(List<T> list)`这个方法完成的排序实际上要求了被排序的类型需要实现Comparable接口完成比较的功能在String类型上如下
```java
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
```
String类实现了这个接口并完成了比较规则的定义但是这样就把这种规则写死了那比如我想要字符串按照第一个字符降序排列那么这样就要修改String的源代码这是不可能的了那么这个时候我们可以使用
`public static <T> void sort(List<T> listComparator<? super T> )`方法灵活的完成这个里面就涉及到了Comparator这个接口位于位于java.util包下排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:
* ` public int compare(String o1, String o2)`:比较其两个参数的顺序。
> 两个对象比较的结果有三种:大于,等于,小于。
>
> 如果要按照升序排序,
> 则o1 小于o2返回负数相等返回001大于02返回正数
> 如果要按照降序排序
> 则o1 小于o2返回正数相等返回001大于02返回负数
操作如下:
```java
public class CollectionsDemo3 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法 按照第一个单词的降序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.charAt(0) - o1.charAt(0);
}
});
System.out.println(list);
}
}
```
结果如下:
```
[sba, nba, cba, aba]
```
### 5.3 简述Comparable和Comparator两个接口的区别。
**Comparable**强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表和数组可以通过Collections.sort和Arrays.sort进行自动排序对象可以用作有序映射中的键或有序集合中的元素无需指定比较器。
**Comparator**强行对某个对象进行整体排序。可以将Comparator 传递给sort方法如Collections.sort或 Arrays.sort从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构如有序set或有序映射的顺序或者为那些没有自然顺序的对象collection提供排序。
### 5.4 练习
创建一个学生类存储到ArrayList集合中完成指定排序操作。
Student 初始类
~~~java
public class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
~~~
测试类:
~~~java
public class Demo {
public static void main(String[] args) {
// 创建四个学生对象 存储到集合中
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("rose",18));
list.add(new Student("jack",16));
list.add(new Student("abc",16));
list.add(new Student("ace",17));
list.add(new Student("mark",16));
/*
让学生 按照年龄排序 升序
*/
// Collections.sort(list);//要求 该list中元素类型 必须实现比较器Comparable接口
for (Student student : list) {
System.out.println(student);
}
}
}
~~~
发现当我们调用Collections.sort()方法的时候 程序报错了。
原因如果想要集合中的元素完成排序那么必须要实现比较器Comparable接口。
于是我们就完成了Student类的一个实现如下
~~~java
public class Student implements Comparable<Student>{
....
@Override
public int compareTo(Student o) {
return this.age-o.age;//升序
}
}
~~~
再次测试代码就OK 了效果如下:
~~~java
Student{name='jack', age=16}
Student{name='abc', age=16}
Student{name='mark', age=16}
Student{name='ace', age=17}
Student{name='rose', age=18}
~~~

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -0,0 +1,606 @@
# day04 【Map、补充知识点】
### 主要内容
- Map集合
- 补充知识点
### 教学目标
- [ ] 能够说出Map集合特点
- [ ] 使用Map集合添加方法保存数据
- [ ] 使用”键找值”的方式遍历Map集合
- [ ] 使用”键值对”的方式遍历Map集合
- [ ] 能够使用HashMap存储自定义键值对的数据
- [ ] 能够使用可变参数
- [ ] 能够使用debug调试程序
- [ ] 能够理解冒泡排序的原理
- [ ] 能够使用Arrays数组工具类的常用方法
- [ ] 能够使用HashMap编写斗地主洗牌发牌案例
## 第一章 Map集合
### 1.1 概述
现实生活中我们常会看到这样的一种集合IP地址与主机名身份证号与个人系统用户名与系统用户对象等这种一一对应的关系就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象`java.util.Map`接口。
我们通过查看`Map`接口描述,发现`Map`接口下的集合与`Collection`接口下的集合,它们存储数据的形式不同,如下图。
![](img\Collection与Map.bmp)
* `Collection`中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
* `Map`中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
* `Collection`中的集合称为单列集合,`Map`中的集合称为双列集合。
* 需要注意的是,`Map`中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。
### 1.2 Map的常用子类
通过查看Map接口描述看到Map有多个子类这里我们主要讲解常用的HashMap集合、LinkedHashMap集合。
* **HashMap\<K,V>**存储数据采用的哈希表结构元素的存取顺序不能保证一致。由于要保证键的唯一、不重复需要重写键的hashCode()方法、equals()方法。
* **LinkedHashMap\<K,V>**HashMap下有个子类LinkedHashMap存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致通过哈希表结构可以保证的键的唯一、不重复需要重写键的hashCode()方法、equals()方法。
> tipsMap接口中的集合都有两个泛型变量\<K,V>,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量\<K,V>的数据类型可以相同,也可以不同。
>
### 1.3 Map的常用方法
Map接口中定义了很多方法常用的如下
* `public V put(K key, V value)`: 把指定的键与指定的值添加到Map集合中。
* `public V remove(Object key)`: 把指定的键 所对应的键值对元素 在Map集合中删除返回被删除元素的值。
* `public V get(Object key)` 根据指定的键在Map集合中获取对应的值。
* `public Set<K> keySet()`: 获取Map集合中所有的键存储到Set集合中。
* `public Set<Map.Entry<K,V>> entrySet()`: 获取到Map集合中所有的键值对对象的集合(Set集合)。
* `public boolean containKey(Object key)`:判断该集合中是否有此键。
Map接口的方法演示
~~~java
public class MapDemo {
public static void main(String[] args) {
//创建 map对象
HashMap<String, String> map = new HashMap<String, String>();
//添加元素到集合
map.put("黄晓明", "杨颖");
map.put("文章", "马伊琍");
map.put("邓超", "孙俪");
System.out.println(map);
//String remove(String key)
System.out.println(map.remove("邓超"));
System.out.println(map);
// 想要查看 黄晓明的媳妇 是谁
System.out.println(map.get("黄晓明"));
System.out.println(map.get("邓超"));
}
}
~~~
> tips:
>
> 使用put方法时若指定的键(key)在集合中没有则没有这个键对应的值返回null并把指定的键值添加到集合中
>
> 若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。
### 1.4 Map的遍历
#### 方式1:键找值方式
通过元素中的键,获取键所对应的值
分析步骤:
1. 获取Map中所有的键由于键是唯一的所以返回一个Set集合存储所有的键。方法提示:`keyset()`
2. 遍历键的Set集合得到每一个键。
3. 根据键,获取键所对应的值。方法提示:`get(K key)`
遍历图解:
![](img\Map集合遍历方式一.bmp)
*
#### 方式2:键值对方式
即通过集合中每个键值对(Entry)对象,获取键值对(Entry)对象中的键与值。
**Entry键值对对象:**
我们已经知道,`Map`中存放的是两种对象,一种称为**key**(键),一种称为**value**(值),它们在在`Map`中是一一对应关系,这一对对象又称做`Map`中的一个`Entry(项)``Entry`将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历`Map`集合时,就可以从每一个键值对(`Entry`)对象中获取对应的键与对应的值。
在Map集合中也提供了获取所有Entry对象的方法
- `public Set<Map.Entry<K,V>> entrySet()`: 获取到Map集合中所有的键值对对象的集合(Set集合)。
获取了Entry对象 , 表示获取了一对键和值那么同样Entry中 , 分别提供了获取键和获取值的方法:
- `public K getKey()`获取Entry对象中的键。
- `public V getValue()`获取Entry对象中的值。
操作步骤与图解:
1. 获取Map集合中所有的键值对(Entry)对象以Set集合形式返回。方法提示:`entrySet()`
2. 遍历包含键值对(Entry)对象的Set集合得到每一个键值对(Entry)对象。
3. 通过键值对(Entry)对象获取Entry对象中的键与值。 方法提示:`getkey() getValue()`
遍历图解:
![](img\Map集合遍历方式二.bmp)
> tipsMap集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。
>
### 1.5 HashMap存储自定义类型
练习每位学生姓名年龄都有自己的家庭住址。那么既然有对应关系则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。
> 注意,学生姓名相同并且年龄相同视为同一名学生。
>
编写学生类:
~~~java
public class Student {
private String name;
private int age;
//构造方法
//get/set
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
~~~
编写测试类:
~~~java
public class HashMapTest {
public static void main(String[] args) {
//1,创建Hashmap集合对象。
Map<Student,String> map = new HashMap<Student,String>();
//2,添加元素。
map.put(new Student("lisi",28), "上海");
map.put(new Student("wangwu",22), "北京");
map.put(new Student("wangwu",22), "南京");
//3,取出元素。键找值方式
Set<Student> keySet = map.keySet();
for(Student key: keySet){
String value = map.get(key);
System.out.println(key.toString()+"....."+value);
}
}
}
~~~
* 当给HashMap中存放自定义对象时如果自定义对象作为key存在这时要保证对象唯一必须复写对象的hashCode和equals方法(如果忘记请回顾HashSet存放自定义对象)。
* 如果要保证map中存放的key和取出的顺序一致可以使用`java.util.LinkedHashMap`集合来存放。
### 1.6 LinkedHashMap介绍
我们知道HashMap保证成对元素唯一并且查询速度很快可是成对元素存放进去是没有顺序的那么我们要保证有序还要速度快怎么办呢
在HashMap下面有一个子类LinkedHashMap它是链表和哈希表组合的一个数据存储结构。
~~~java
public class LinkedHashMapDemo {
public static void main(String[] args) {
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("邓超", "孙俪");
map.put("李晨", "范冰冰");
map.put("刘德华", "朱丽倩");
Set<Entry<String, String>> entrySet = map.entrySet();
for (Entry<String, String> entry : entrySet) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
~~~
结果:
~~~
邓超 孙俪
李晨 范冰冰
刘德华 朱丽倩
~~~
### 1.7 Map集合练习
**需求:**
计算一个字符串中每个字符出现次数。
**分析:**
1. 获取一个字符串对象
2. 创建一个Map集合键代表字符值代表次数。
3. 遍历字符串得到每个字符。
4. 判断Map中是否有该键。
5. 如果没有第一次出现存储次数为1如果有则说明已经出现过获取到对应的值进行++,再次存储。
6. 打印最终结果
**方法介绍**
`public boolean containKey(Object key)`:判断该集合中是否有此键。
**代码:**
~~~java
public class MapTest {
public static void main(String[] args) {
//友情提示
System.out.println("请录入一个字符串:");
String line = new Scanner(System.in).nextLine();
// 定义 每个字符出现次数的方法
findChar(line);
}
private static void findChar(String line) {
//1:创建一个集合 存储 字符 以及其出现的次数
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
//2:遍历字符串
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
//判断 该字符 是否在键集中
if (!map.containsKey(c)) {//说明这个字符没有出现过
//那就是第一次
map.put(c, 1);
} else {
//先获取之前的次数
Integer count = map.get(c);
//count++;
//再次存入 更新
map.put(c, ++count);
}
}
System.out.println(map);
}
}
~~~
## 第二章 补充知识点
### 2.1 可变参数
**JDK1.5**之后如果我们定义一个方法需要接受多个参数并且多个参数类型一致我们可以对其简化.
**格式:**
```
修饰符 返回值类型 方法名(参数类型... 形参名){ }
```
**代码演示:**
```java
public class ChangeArgs {
public static void main(String[] args) {
int sum = getSum(6, 7, 2, 12, 2121);
System.out.println(sum);
}
public static int getSum(int... arr) {
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
}
```
**注意:**
1.一个方法只能有一个可变参数
2.如果方法中有多个参数可变参数要放到最后
**应用场景: Collections**
在Collections中也提供了添加一些元素方法
`public static <T> boolean addAll(Collection<T> c, T... elements) `:往集合中添加一些元素
**代码演示:**
```java
public class CollectionsDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
//原来写法
//list.add(12);
//list.add(14);
//list.add(15);
//list.add(1000);
//采用工具类 完成 往集合中添加元素
Collections.addAll(list, 5, 222, 12);
System.out.println(list);
}
```
### 2.2 Debug追踪
**使用IDEA的断点调试功能查看程序的运行过程** Debug调试窗口介绍
![](img\debug5.png)
### 2.3 数组排序
#### 冒泡排序
##### 冒泡排序概述
- 一种排序的方式对要进行排序的数据中相邻的数据进行两两比较将较大的数据放在后面依次对所有的数据进行操作直至所有数据按要求完成排序
- 如果有n个数据进行排序总共需要比较n-1次
- 每一次比较完毕下一次的比较就会少一个数据参与
##### 冒泡排序图解
![5](img/5.png)
##### 冒泡排序代码实现
```java
/*
冒泡排序:
一种排序的方式,对要进行排序的数据中相邻的数据进行两两比较,将较大的数据放在后面,
依次对所有的数据进行操作,直至所有数据按要求完成排序
*/
public class ArrayDemo {
public static void main(String[] args) {
//定义一个数组
int[] arr = {7, 6, 5, 4, 3};
System.out.println("排序前:" + arrayToString(arr));
// 这里减1是控制每轮比较的次数
for (int x = 0; x < arr.length - 1; x++) {
// -1是为了避免索引越界-x是为了调高比较效率
for (int i = 0; i < arr.length - 1 - x; i++) {
if (arr[i] > arr[i + 1]) {
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}
}
System.out.println("排序后:" + arrayToString(arr));
}
//把数组中的元素按照指定的规则组成一个字符串:[元素1, 元素2, ...]
public static String arrayToString(int[] arr) {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < arr.length; i++) {
if (i == arr.length - 1) {
sb.append(arr[i]);
} else {
sb.append(arr[i]).append(", ");
}
}
sb.append("]");
String s = sb.toString();
return s;
}
}
```
### 2.4 Arrays类
#### 概述
`java.util.Arrays` 此类包含用来操作数组的各种方法比如排序和搜索等其所有方法均为静态方法调用起来非常简单
#### 操作数组的方法
- `public static String toString(int[] a) ` 返回指定数组内容的字符串表示形式
```java
public static void main(String[] args) {
// 定义int 数组
int[] arr = {7, 6, 5, 4, 3};
// 打印数组,输出地址值
System.out.println(arr); // [I@2ac1fdc4
// 数组内容转为字符串
String s = Arrays.toString(arr);
// 打印字符串,输出内容
System.out.println(s); // [7, 6, 5, 4, 3]
}
```
- `public static void sort(int[] a)` 对指定的 int 型数组按数字升序进行排序
```java
public static void main(String[] args) {
// 定义int 数组
int[] arr = {7, 6, 5, 4, 3};
System.out.println("排序前:"+ Arrays.toString(arr)); // 排序前:[7, 6, 5, 4, 3]
// 升序排序
Arrays.sort(arr);
System.out.println("排序后:"+ Arrays.toString(arr));// 排序后:[3, 4, 5, 6, 7]
}
```
## 第三章 模拟斗地主洗牌发牌
### 3.1 案例介绍
按照斗地主的规则完成洗牌发牌的动作
![](img\斗地主.png)
具体规则
1. 组装54张扑克牌将
2. 54张牌顺序打乱
3. 三个玩家参与游戏三人交替摸牌每人17张牌最后三张留作底牌
4. 查看三人各自手中的牌按照牌的大小排序)、底牌
> 规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3
>
### 3.2 案例需求分析
1. 准备牌:
完成数字与纸牌的映射关系:
使用双列Map(HashMap)集合,完成一个数字与字符串纸牌的对应关系(相当于一个字典)。
2. 洗牌:
通过数字完成洗牌发牌
3. 发牌:
将每个人以及底牌设计为ArrayList\<String>,将最后3张牌直接存放于底牌剩余牌通过对3取模依次发牌。
存放的过程中要求数字大小与斗地主规则的大小对应。
将代表不同纸牌的数字分配给不同的玩家与底牌。
4. 看牌:
通过Map集合找到对应字符展示。
通过查询纸牌与数字的对应关系,由数字转成纸牌字符串再进行展示。
![](img\斗地主分析.png)
### 3.3 实现代码步骤
~~~java
package com.inmind04;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
/*
* 组合牌
* 定义一个Map集合用来存储牌号 和 牌
* 定义一个List集合用来存储牌号
* 花色:♥-♠-♦-♣
* 数字:2-A-K-Q-J-10-9-8-7-6-5-4-3
* 洗牌
* Collections.shuffle(牌号集合)
* 发牌
* 三个玩家三个集合
* 发牌号
* 排序
* 看牌
*/
public class Pooker {
public static void main(String[] args) {
// 定义一个Map集合用来存储牌号 和 牌
HashMap<Integer, String> pookerMap = new HashMap<Integer, String>();
//定义一个List集合用来存储牌号
ArrayList<Integer> pookerList = new ArrayList<Integer>();
String[] colors = "♥-♠-♦-♣".split("-");
String[] nums = "2-A-K-Q-J-10-9-8-7-6-5-4-3".split("-");
int index = 2;
for(String num : nums){
for(String color : colors){
String thisPooker = color+num;
// System.out.println(thisPooker);
//将扑克牌放入Map集合
pookerMap.put(index, thisPooker);
//将牌号放入到pookerList集合中
pookerList.add(index);
index++;
}
}
//将大王小王添加到集合
pookerMap.put(0, "大王");
pookerMap.put(1, "小王");
pookerList.add(0);
pookerList.add(1);
// System.out.println(pookerMap);
// System.out.println(pookerList);
//洗牌
Collections.shuffle(pookerList);
//发牌
ArrayList<Integer> player1 = new ArrayList<Integer>();
ArrayList<Integer> player2 = new ArrayList<Integer>();
ArrayList<Integer> player3 = new ArrayList<Integer>();
ArrayList<Integer> diPai = new ArrayList<Integer>();
//遍历牌号的集合 判断索引发牌号
for(int i = 0 ;i < pookerList.size() ;i++){
Integer pookerNum = pookerList.get(i);
if(i>=51)
diPai.add(pookerNum);
}else if(i % 3 == 0){
player1.add(pookerNum);
}else if(i % 3 == 1){
player2.add(pookerNum);
}else if(i % 3 == 2){
player3.add(pookerNum);
}
}
// 排序
Collections.sort(player1);
Collections.sort(player2);
Collections.sort(player3);
Collections.sort(diPai);
// System.out.println(player1);
// System.out.println(player2);
// System.out.println(player3);
// System.out.println(diPai);
show("柳岩",player1,pookerMap);
show("唐嫣",player2,pookerMap);
show("金莲",player3,pookerMap);
show("底牌",diPai,pookerMap);
}
//定义方法 看牌
public static void show(String name,ArrayList<Integer> player,HashMap<Integer, String> pookerMap ){
System.out.print(name+":");
for(Integer pookerNum : player){
String thisPooker = pookerMap.get(pookerNum);
System.out.print(thisPooker+" ");
}
System.out.println();
}
}
~~~

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,563 @@
# day05 【异常、线程】
### 主要内容
- 异常、线程
### 教学目标
- [ ] 能够辨别程序中异常和错误的区别
- [ ] 说出异常的分类
- [ ] 说出虚拟机处理异常的方式
- [ ] 列举出常见的三个运行期异常
- [ ] 能够使用try...catch关键字处理异常
- [ ] 能够使用throws关键字处理异常
- [ ] 能够自定义异常类
- [ ] 能够处理自定义异常类
- [ ] 说出进程的概念
- [ ] 说出线程的概念
- [ ] 能够理解并发与并行的区别
- [ ] 能够开启新线程
## 第一章 异常
### 1.1 异常概念
异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是:
* **异常** 指的是程序在执行过程中出现的非正常的情况最终会导致JVM的非正常停止。
在Java等面向对象的编程语言中异常本身是一个类产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。
> 异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.
### 1.2 异常体系
异常机制其实是帮助我们**找到**程序中的问题,异常的根类是`java.lang.Throwable`,其下有两个子类:`java.lang.Error``java.lang.Exception`,平常所说的异常指`java.lang.Exception`
![](img\异常体系.png)
**Throwable体系**
* **Error**:严重错误Error无法通过处理的错误只能事先避免好比绝症。
* **Exception**:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。
**Throwable中的常用方法**
* `public void printStackTrace()`:打印异常的详细信息。
*包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。*
* `public String getMessage()`:获取发生异常的原因。
*提示给用户的时候,就提示错误原因。*
* `public String toString()`:获取异常的类型和异常描述信息(不用)。
***出现异常,不要紧张,把异常的简单类名,拷贝到API中去查。***
![](img\简单的异常查看.bmp)
### 1.3 异常分类
我们平常说的异常就是指Exception因为这类异常一旦出现我们就要对代码进行更正修复程序。
**异常(Exception)的分类**:根据在编译时期还是运行时期去检查异常?
* **编译时期异常**:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
* **运行时期异常**:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)
![](img\异常的分类.png)
### 1.4 异常的产生过程解析
先运行下面的程序程序会产生一个数组索引越界异常ArrayIndexOfBoundsException。我们通过图解来解析下异常产生的过程。
工具类
~~~java
public class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}
~~~
测试类
~~~java
public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
intnum = ArrayTools.getElement(arr, 4)
System.out.println("num=" + num);
System.out.println("over");
}
}
~~~
上述程序执行过程图解:
![](img\异常产生过程.png)
## 第二章 异常的处理
Java异常处理的五个关键字**try、catch、finally、throw、throws**
### 2.1 抛出异常throw
在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。
在java中提供了一个**throw**关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢?
1. 创建一个异常对象。封装一些提示信息(信息可以自己编写)。
2. 需要将这个异常对象告知给调用者。怎么告知呢怎么将这个异常对象传递到调用者处呢通过关键字throw就可以完成。throw 异常对象。
throw**用在方法内**,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。
**使用格式:**
~~~
throw new 异常类名(参数);
~~~
例如:
~~~java
throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
~~~
学习完抛出异常的格式后我们通过下面程序演示下throw的使用。
~~~java
public class ThrowDemo {
public static void main(String[] args) {
//创建一个数组
int[] arr = {2,4,52,2};
//根据索引找对应的元素
int index = 4;
int element = getElement(arr, index);
System.out.println(element);
System.out.println("over");
}
/*
* 根据 索引找到数组中对应的元素
*/
public static int getElement(int[] arr,int index){
//判断 索引是否越界
if(index<0 || index>arr.length-1){
/*
判断条件如果满足当执行完throw抛出异常对象后方法已经无法继续运算。
这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。
*/
throw new ArrayIndexOutOfBoundsException("哥们,角标越界了~~~");
}
int element = arr[index];
return element;
}
}
~~~
> 注意如果产生了问题我们就会throw将问题描述类即异常进行抛出也就是将问题返回给该方法的调用者。
>
> 那么对于调用者来说该怎么处理呢一种是进行捕获处理另一种就是继续讲问题声明出去使用throws声明处理。
### 2.2 Objects非空判断
还记得我们学习过一个类Objects吗曾经提到过它由一些静态的实用方法组成这些方法是null-save空指针安全的或null-tolerant容忍空指针的那么在它的源码中对对象为null的值进行了抛出异常操作。
* `public static <T> T requireNonNull(T obj)`:查看指定引用对象不是null。
查看源码发现这里对为null的进行了抛出异常操作
~~~java
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
~~~
### 2.3 声明异常throws
**声明异常**将问题标识出来报告给调用者。如果方法内通过throw抛出了编译时异常而没有捕获处理稍后讲解该方式那么必须通过throws进行声明让调用者去处理。
关键字**throws**运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).
**声明异常格式:**
~~~
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
~~~
声明异常的代码演示:
~~~java
public class ThrowsDemo {
public static void main(String[] args) throws FileNotFoundException {
read("a.txt");
}
// 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
}
}
~~~
throws用于进行异常类的声明若该方法可能有多种异常情况产生那么在throws后面可以写多个异常类用逗号隔开。
~~~java
public class ThrowsDemo2 {
public static void main(String[] args) throws IOException {
read("a.txt");
}
public static void read(String path)throws FileNotFoundException, IOException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
if (!path.equals("b.txt")) {
throw new IOException();
}
}
}
~~~
### 2.4 捕获异常try…catch
如果异常出现的话,会立刻终止程序,所以我们得处理异常:
1. 该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
2. 在方法中使用try-catch的语句块来处理异常。
**try-catch**的方式就是捕获异常。
* **捕获异常**Java中对异常有针对性的语句进行捕获可以对出现的异常进行指定方式的处理。
捕获异常语法如下:
~~~java
try{
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
~~~
**try**该代码块中编写可能产生异常的代码。
**catch**用来进行某种异常的捕获,实现对捕获到的异常进行处理。
> 注意:try和catch都不能单独使用,必须连用。
演示如下:
~~~java
public class TryCatchDemo {
public static void main(String[] args) {
try {// 当产生异常时,必须有处理方式。要么捕获,要么声明。
read("b.txt");
} catch (FileNotFoundException e) {// 括号中需要定义什么呢?
//try中抛出的是什么异常在括号中就定义什么异常类型
System.out.println(e);
}
System.out.println("over");
}
/*
*
* 我们 当前的这个方法中 有异常 有编译期异常
*/
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
}
}
~~~
如何获取异常信息:
Throwable类中定义了一些查看方法:
* `public String getMessage()`:获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。
* `public String toString()`:获取异常的类型和异常描述信息(不用)。
* `public void printStackTrace()`:打印异常的跟踪栈信息并输出到控制台。
*包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。*
在开发中呢也可以在catch将编译期异常转换成运行期异常处理。
多个异常使用捕获又该如何处理呢?
1. 多个异常分别处理。
2. 多个异常一次捕获,多次处理。
3. 多个异常一次捕获一次处理。
一般我们是使用一次捕获多次处理方式,格式如下:
~~~java
try{
编写可能会出现异常的代码
}catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
~~~
> 注意:这种异常处理方式要求多个catch中的异常不能相同并且若catch中的多个异常之间有子父类异常的关系那么子类异常要求在上面的catch处理父类异常在下面的catch处理。
### 2.4 finally 代码块
**finally**有一些特定的代码无论异常是否发生都需要执行。另外因为异常会引发程序跳转导致有些语句执行不到。而finally就是解决这个问题的在finally代码块中存放的代码都是一定会被执行的。
什么时候的代码必须最终执行?
当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。
finally的语法:
try...catch....finally:自身需要处理异常,最终还得关闭资源。
> 注意:finally不能单独使用。
比如在我们之后学习的IO流中当打开了一个关联文件的资源最后程序不管结果如何都需要把这个资源关闭掉。
finally代码参考如下
~~~java
public class TryCatchDemo4 {
public static void main(String[] args) {
try {
read("a.txt");
} catch (FileNotFoundException e) {
//抓取到的是编译期异常 抛出去的是运行期
throw new RuntimeException(e);
} finally {
System.out.println("不管程序怎样,这里都将会被执行。");
}
System.out.println("over");
}
/*
*
* 我们 当前的这个方法中 有异常 有编译期异常
*/
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
}
}
~~~
> 当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。
![](img\死了都要try.bmp)
### 2.5 异常注意事项
* 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
* 如果父类抛出了多个异常,子类覆盖父类方法时,只能抛出相同的异常或者是他的子集。
* 父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
* 当多异常处理时,捕获处理,前边的类不能是后边类的父类
* 在try/catch后可以追加finally代码块其中的代码一定会被执行通常用于资源回收。
* 如果finally有return语句,永远返回finally中的结果,避免该情况.
## 第三章 自定义异常
### 3.1 概述
**为什么需要自定义异常类:**
我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,此时我们根据自己业务的异常情况来定义异常类。,例如年龄负数问题,考试成绩负数问题。
在上述代码中发现这些异常都是JDK内部定义好的但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?
**什么是自定义异常类:**
在开发中根据自己业务的异常情况来定义异常类.
自定义一个业务逻辑异常: **LoginException**。一个登陆异常类。
**异常类如何定义:**
1. 自定义一个编译期异常: 自定义类 并继承于`java.lang.Exception`
2. 自定义一个运行时期的异常类:自定义类 并继承于`java.lang.RuntimeException`
### 3.2 自定义异常的练习
要求:我们模拟登陆操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。
首先定义一个登陆异常类LoginException
~~~java
// 业务逻辑异常
public class LoginException extends Exception {
/**
* 空参构造
*/
public LoginException() {
}
/**
*
* @param message 表示异常提示
*/
public LoginException(String message) {
super(message);
}
}
~~~
模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。
~~~java
public class Demo {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};
public static void main(String[] args) {
//调用方法
try{
// 可能出现异常的代码
checkUsername("nill");
System.out.println("注册成功");//如果没有异常就是注册成功
}catch(LoginException e){
//处理异常
e.printStackTrace();
}
}
//判断当前注册账号是否存在
//因为是编译期异常,又想调用者去处理 所以声明该异常
public static boolean checkUsername(String uname) throws LoginException{
for (String name : names) {
if(name.equals(uname)){//如果名字在这里面 就抛出登陆异常
throw new LoginException("亲"+name+"已经被注册了!");
}
}
return true;
}
}
~~~
## 第四章 多线程
我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?
要解决上述问题,咱们得使用多进程或者多线程来解决.
### 4.1 并发与并行
* **并行**:指两个或多个事件在**同一时刻**发生(同时发生)。
* **并发**:指两个或多个事件在**同一个时间段内**发生。
![](img\并行与并发.bmp)
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中则这些可以并发执行的程序便可以分配到多个处理器上CPU实现多任务并行执行即利用每个处理器来处理一个可以并发执行的程序这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU便是多核处理器核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。
> 注意单核处理器的计算机肯定是不能并行的处理多个任务的只能是多个任务在单个CPU上并发运行。同理,线程也是一样的从宏观角度上理解线程是并行运行的但是从微观角度上分析却是串行运行的即一个线程一个线程的去运行当系统只有一个CPU时线程会以某种顺序执行多个线程我们把这种情况称之为线程调度。
### 4.2 线程与进程
* **进程**:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
* **线程**:进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。
我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程和线程:
**进程**
![](img\进程概念.png)
**线程**
![](img\线程概念.png)
**进程与线程的区别**
* 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
* 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
**注意:**
1. 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
2. Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM每一个 JVM 实际上就是在操作系统中启动了一个线程java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
3. 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
**线程调度:**
计算机通常只有一个CPU时,在任意时刻只能执行一条计算机指令,每一个进程只有获得CPU的使用权才能执行指令。所谓多进程并发运行,从宏观上看,其实是各个进程轮流获得CPU的使用权,分别执行各自的任务。那么,在可运行池中,会有多个线程处于就绪状态等到CPU,JVM就负责了线程的调度。JVM采用的是**抢占式调度**,没有采用分时调度,因此可以能造成多线程执行结果的的随机性。
### 4.3 创建线程类
Java使用`java.lang.Thread`类代表**线程**所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来**创建**并**启动多线程**的步骤如下:
1. 定义Thread类的子类并重写该类的run()方法该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
2. 创建Thread子类的实例即创建了线程对象
3. 调用线程对象的start()方法来启动该线程
代码如下:
测试类:
~~~java
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程"+i);
}
}
}
~~~
自定义线程类
~~~java
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法指定线程的名称
super(name);
}
/**
* 重写run方法完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"正在执行"+i);
}
}
}
~~~
OK 相信我们都看到多线程的现象了那么接下来几天我们就进入多线程的世界
那么现在有个小问题请问植物大战僵尸游戏中是用了多进程设计程序呢还是多线程
![](img\植物大战僵尸.bmp)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -0,0 +1,763 @@
# day06 【线程、同步】
### 主要内容
- 线程
- 同步
- 线程状态
### 教学目标
- [ ] 能够描述Java中多线程运行原理
- [ ] 能够使用继承类的方式创建多线程
- [ ] 能够使用实现接口的方式创建多线程
- [ ] 能够说出实现接口方式的好处
- [ ] 能够解释安全问题的出现的原因
- [ ] 能够使用同步代码块解决线程安全问题
- [ ] 能够使用同步方法解决线程安全问题
- [ ] 能够理解线程通信概念
- [ ] 能够理解等待唤醒机制
- [ ] 能够说出线程6个状态的名称
## 第一章 线程
### 1.1 多线程原理
昨天的时候我们已经写过一版多线程的代码,很多同学对原理不是很清楚,那么我们今天先画个多线程执行时序图来体现一下多线程程序的执行流程。
代码如下:
自定义线程类:
~~~javascript
public class MyThread extends Thread{
/*
* 利用继承中的特点
* 将线程名称传递 进行设置
*/
public MyThread(String name){
super(name);
}
/*
* 重写run方法
* 定义线程要执行的代码
*/
public void run(){
for (int i = 0; i < 20; i++) {
//getName()方法 来自父亲
System.out.println(getName()+i);
}
}
}
~~~
测试类
~~~java
public class Demo {
public static void main(String[] args) {
System.out.println("这里是main线程");
MyThread mt = new MyThread("小强");
mt.start();//开启了一个新的线程
for (int i = 0; i < 20; i++) {
System.out.println("旺财:"+i);
}
}
}
~~~
流程图
![](img\线程流程图.png)
程序启动运行main时候java虚拟机启动一个进程主线程main在main()调用时候被创建随着调用mt的对象的start方法另外一个新的线程也启动了这样整个应用就在多线程下运行
通过这张图我们可以很清晰的看到多线程的执行流程那么为什么可以完成并发执行呢我们再来讲一讲原理
多线程执行时到底在内存中是如何运行的呢以上个程序为例进行图解说明
多线程执行时在栈内存中其实**每一个执行线程都有一片自己所属的栈内存空间**。进行方法的压栈和弹栈
![](img\栈内存原理图.bmp)
当执行线程的任务结束了线程自动在栈内存中释放了但是当所有的执行线程都结束了那么进程就结束了
### 1.2 Thread类
在上一天内容中我们已经可以完成最基本的线程开启那么在我们完成操作过程中用到了`java.lang.Thread`API中该类中定义了有关线程的一些方法具体如下
**构造方法:**
* `public Thread()`:分配一个新的线程对象
* `public Thread(String name)`:分配一个指定名字的新的线程对象
* `public Thread(Runnable target)`:分配一个带有指定目标新的线程对象
* `public Thread(Runnable target,String name)`:分配一个带有指定目标新的线程对象并指定名字
**常用方法:**
* `public String getName()`:获取当前线程名称
* `public void start()`:导致此线程开始执行; Java虚拟机调用此线程的run方法
* `public void run()`:此线程要执行的任务在此处定义代码
* `public static void sleep(long millis)`:使当前正在执行的线程以指定的毫秒数暂停暂时停止执行)。
* `public static Thread currentThread() `:返回对当前正在执行的线程对象的引用
翻阅API后得知创建线程的方式总共有两种一种是继承Thread类方式一种是实现Runnable接口方式方式一我们上一天已经完成接下来讲解方式二实现的方式
### 1.3 创建线程方式二
采用`java.lang.Runnable`也是非常常见的一种我们只需要重写run方法即可
步骤如下
1. 定义Runnable接口的实现类并重写该接口的run()方法该run()方法的方法体同样是该线程的线程执行体
2. 创建Runnable实现类的实例并以此实例作为Thread的target来创建Thread对象该Thread对象才是真正的线程对象
3. 调用线程对象的start()方法来启动线程
代码如下
~~~java
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
~~~
~~~java
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
~~~
通过实现Runnable接口使得该类有了多线程类的特征run()方法是多线程程序的一个执行目标所有的多线程代码都在run方法里面Thread类实际上也是实现了Runnable接口的类
在启动的多线程的时候需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象然后调用Thread对象的start()方法来运行多线程代码
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的因此不管是继承Thread类还是实现Runnable接口来实现多线程最终还是通过Thread的对象的API来控制线程的熟悉Thread类的API是进行多线程编程的基础
> tips:Runnable对象仅仅作为Thread对象的targetRunnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例只是该Thread线程负责执行其target的run()方法。
### 1.4 Thread和Runnable的区别
**A避免了Java单继承的局限性**
说明如果使用方式一那么在Java中一个类只能有一个直接父类如果一个类已经继承其他的父类那么当前这个类中假如有需要多线程操作的代码这时这个类是无法再继承Thread类的这样就会导致当前这个类中的某些需要多线程执行的任务代码就无法被线程去执行
**B把线程代码和任务的代码分离解耦合(解除线程代码和任务的代码模块之间的依赖关系)。代码的扩展性非常好;**
说明Thread类是专门负责描述线程本身的Thread类可以对线程进行各种各样的操作如果使用第一种方式那么把线程要执行的任务也交给了Thread类这样就会导致操作线程本身的功能和线程要执行的任务功能严重的耦合在一起
但是方式二自定义一个类来实现Runnable接口这样就把任务抽取到Runnable接口中在这个接口中定义线程需要执行的任务的规则当需要明确线程的任务时我们就让这个类实现Runnable接口只要实现Runnable接口的类就相当于明确了线程需要执行的任务
当一个类实现Runnable接口就相当于有了线程的任务可是还没有线程本身这个对象这时我们就可以直接使用Thread这个类创建出线程然后把任务交给线程这样就达到任务和线程的分离以及结合
### 1.5 匿名内部类方式实现线程的创建
使用线程的内匿名内部类方式可以方便的实现每个线程执行不同的线程任务操作
使用匿名内部类的方式实现Runnable接口重新Runnable接口中的run方法
~~~java
public class NoNameInnerClassThread {
public static void main(String[] args) {
// new Runnable(){
// public void run(){
// for (int i = 0; i < 20; i++) {
// System.out.println("张宇:"+i);
// }
// }
// }; //---这个整体 相当于new MyRunnable()
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
}
}
~~~
## 第二章 线程安全
### 2.1 线程安全
如果有多个线程在同时运行而这些线程可能会同时运行这段代码程序每次运行结果和单线程运行的结果是一样的而且其他的变量的值也和预期的是一样的就是线程安全的
我们通过一个案例演示线程的安全问题
电影院要卖票我们模拟电影院的卖票过程假设要播放的电影是 葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。
我们来模拟电影院的售票窗口实现多个窗口同时卖 葫芦娃大战奥特曼这场电影票(多个窗口一起卖这100张票)
需要窗口采用线程对象来模拟需要票Runnable接口子类来模拟
模拟票
~~~java
public class Ticket implements Runnable {
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
~~~
测试类:
~~~java
public class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
~~~
结果中有一部分这样现象:
![](img\线程安全问题.png)
发现程序出现了两个问题:
1. 相同的票数,比如5这张票被卖了两回。
2. 不存在的票比如0票与-1票是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
> 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
### 2.2 线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题Java中提供了同步机制(**synchronized**)来解决。
根据案例简述:
~~~
窗口1线程进入操作的时候窗口2和窗口3线程只能在外等着窗口1操作结束窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候其他线程不能修改该资源等待修改完毕同步之后才能去抢夺CPU资源完成对应的操作保证了数据的同步性解决了线程不安全的现象。
~~~
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:
1. 同步代码块。
2. 同步方法。
3. 锁机制。
### 2.3 同步代码块
* **同步代码块**`synchronized`关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
~~~java
synchronized(同步锁){
需要同步操作的代码
}
~~~
**同步锁**:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
1. 锁对象 可以是任意类型。
2. 多个线程对象 要使用同一把锁。
> 注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块解决代码:
~~~java
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
synchronized (lock) {
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
}
}
~~~
当使用了同步代码块后,上述的线程的安全问题,解决了。
### 2.4 同步方法
* **同步方法**:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
~~~java
public synchronized void method(){
可能会产生线程安全问题的代码
}
~~~
> 同步锁是谁?
>
> 对于非static方法,同步锁就是this。
>
> 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
使用同步方法代码如下:
~~~java
public class Ticket implements Runnable{
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
sellTicket();
}
}
/*
* 锁对象 是 谁调用这个方法 就是谁
* 隐含 锁对象 就是 this
*
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
~~~
### 2.5 Lock锁
`java.util.concurrent.locks.Lock`机制提供了比**synchronized**代码块和**synchronized**方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁加锁与释放锁方法化了如下
* `public void lock() `:加同步锁。
* `public void unlock()`:释放同步锁。
使用如下:
~~~java
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();
}
}
}
~~~
## 第三章 等待唤醒机制
### 3.1 线程间通信
**概念:**多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
比如线程A用来生成包子的线程B用来吃包子的包子可以理解为同一资源线程A与线程B处理的动作一个是生产一个是消费那么线程A与线程B之间就存在线程通信问题。
**为什么要处理线程间通信:**
多个线程并发执行时, 在默认情况下CPU是随机切换线程的当我们需要多个线程来共同完成一件任务并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
**如何保证线程间通信有效利用资源:**
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— **等待唤醒机制。**
### 3.2 等待唤醒机制
**什么是等待唤醒机制**
这是多个线程间的一种**协作**机制。谈到线程我们经常想到的是线程间的**竞争race**,比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
就是在一个线程进行了规定操作后,就进入等待状态(**wait()** 等待其他线程执行完他们的指定代码过后 再将其唤醒(**notify()**;在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
**等待唤醒中的方法**
等待唤醒机制就是用于解决线程间通信的问题的使用到的3个方法的含义如下
1. wait线程不再活动不再参与调度进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要执行一个**特别的动作**,也即是“**通知notify**”在这个对象上等待的线程从wait set 中释放出来重新进入到调度队列ready queue
2. notify则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
3. notifyAll则释放所通知对象的 wait set 上的全部线程。
> 注意:
>
> 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
>
> 总结如下:
>
> - 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
> - 否则,从 wait set 出来,又进入 entry set线程就从 WAITING 状态又变成 BLOCKED 状态
**调用wait和notify方法需要注意的细节**
1. wait方法与notify方法必须要由同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2. wait方法与notify方法是属于Object类的方法的。因为锁对象可以是任意对象而任意对象的所属类都是继承了Object类的。
3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为必须要通过锁对象调用这2个方法。
### 3.3 生产者与消费者问题
等待唤醒机制其实就是经典的“生产者与消费者”的问题。
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:
```java
包子铺线程生产包子吃货线程消费包子当包子没有时包子状态为false吃货线程等待包子铺线程生产包子即包子状态为true并通知吃货线程解除吃货的等待状态,因为已经有包子了那么包子铺线程进入等待状态接下来吃货线程能否进一步执行则取决于锁的获取情况如果吃货获取到锁那么就执行吃包子动作包子吃完包子状态为false并通知包子铺线程解除包子铺的等待状态,吃货线程进入等待包子铺线程能否进一步执行则取决于锁的获取情况
```
**代码演示:**
包子资源类:
```java
public class BaoZi {
String pier ;
String xianer ;
boolean flag = false ;//包子资源 是否存在 包子资源状态
}
```
吃货线程类:
```java
public class ChiHuo extends Thread{
private BaoZi bz;
public ChiHuo(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
while(true){
synchronized (bz){
if(bz.flag == false){//没包子
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
bz.flag = false;
bz.notify();
}
}
}
}
```
包子铺线程类:
```java
public class BaoZiPu extends Thread {
private BaoZi bz;
public BaoZiPu(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
int count = 0;
//造包子
while(true){
//同步
synchronized (bz){
if(bz.flag == true){//包子资源 存在
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 没有包子 造包子
System.out.println("包子铺开始做包子");
if(count%2 == 0){
// 冰皮 五仁
bz.pier = "冰皮";
bz.xianer = "五仁";
}else{
// 薄皮 牛肉大葱
bz.pier = "薄皮";
bz.xianer = "牛肉大葱";
}
count++;
bz.flag=true;
System.out.println("包子造好了:"+bz.pier+bz.xianer);
System.out.println("吃货来吃吧");
//唤醒等待线程 (吃货)
bz.notify();
}
}
}
}
```
测试类:
```java
public class Demo {
public static void main(String[] args) {
//等待唤醒案例
BaoZi bz = new BaoZi();
ChiHuo ch = new ChiHuo("吃货",bz);
BaoZiPu bzp = new BaoZiPu("包子铺",bz);
ch.start();
bzp.start();
}
}
```
执行效果:
```java
包子铺开始做包子
包子造好了冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
包子铺开始做包子
包子造好了薄皮牛肉大葱
吃货来吃吧
吃货正在吃薄皮牛肉大葱包子
包子铺开始做包子
包子造好了冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
```
## 第四章 线程状态
### 4.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。 |
| Teminated(被终止) | 因为run方法正常退出而死亡或者因为没有捕获的异常终止了run方法而死亡。 |
我们不需要去研究这几种状态的实现原理我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢新建与被终止还是很容易理解的我们就研究一下线程从Runnable可运行状态与非运行状态之间的转换问题。
### 4.2 Timed Waiting计时等待
Timed Waiting在API中的描述为一个正在限时等待另一个线程执行一个唤醒动作的线程处于这一状态。单独的去理解这句话真是玄之又玄其实我们在之前的操作中已经接触过这个状态了在哪里呢
在我们写卖票的案例中为了减少线程执行太快现象不明显等问题我们在run方法中添加了sleep语句这样就强制当前正在执行的线程休眠**暂停执行**),以“减慢线程”。
其实当我们调用了sleep方法之后当前执行的线程就进入到“休眠状态”其实就是所谓的Timed Waiting(计时等待),那么我们通过一个案例加深对该状态的一个理解。
**实现一个计数器计数到100在每个数字之间暂停1秒每隔10个数字输出一个字符串**
代码:
~~~java
public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
if ((i) % 10 == 0) {
System.out.println("-------" + i);
}
System.out.print(i);
try {
Thread.sleep(1000);
System.out.print(" 线程睡眠1秒\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new MyThread().start();
}
}
~~~
通过案例可以发现sleep方法的使用还是很简单的我们需要记住下面几点
1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法单独的线程也可以调用不一定非要有协作关系
2. 为了让其他线程有机会执行可以将Thread.sleep()的调用**放线程run()之内**。这样才能保证该线程执行过程中会睡眠
3. sleep与锁无关线程睡眠到期自动苏醒并返回到Runnable可运行状态
> 小提示sleep()中指定的时间是线程不会运行的最短时间。因此sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
Timed Waiting 线程状态图![](img\计时等待.png)
### 4.3 BLOCKED锁阻塞
Blocked状态在API中的介绍为一个正在阻塞等待一个监视器锁锁对象的线程处于这一状态
我们已经学完同步机制那么这个状态是非常好理解的了比如线程A与线程B代码中使用同一锁如果线程A获取到锁线程A进入到Runnable状态那么线程B就进入到Blocked锁阻塞状态
这是由Runnable状态进入Blocked状态除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态而这部分内容作为扩充知识点带领大家了解一下
Blocked 线程状态图![](img\锁阻塞.png)
### 4.4 Waiting无限等待
**Wating状态**一个正在无限期等待另一个线程执行一个特别的唤醒动作的线程处于这一状态
那么我们之前遇到过这种状态吗答案是并没有但并不妨碍我们进行一个简单深入的了解我们通过一段代码即生产者和消费者线程来学习一下
注意
让线程无线等待我们需要使用wait方法而唤醒线程应该使用notify(唤醒单个线程)或者notifyAll(唤醒所有线程)方法所以我们先熟悉下这几个方法
A:wait()让线程等待
B:notify()唤醒某个等待中的线程
C:notifyAll()唤醒所有等待的线程
通过查阅API我们发现这三个方法都位于Object类中并不是Thread类中
问题为什么这几个方法要定义在Object类中呢
思考线程的等待和唤醒应该由谁来控制
线程有很多控制线程的一定是跟多个线程都有关系的对象--------锁对象
锁对象本来就是控制线程运行的因此线程的等待和唤醒就交给锁来控制
线程等待和唤醒应该由锁来完成
锁对象可以是任意的任意对象都可以调用这几个方法任意对象都可以调用的方法必须定义在Object类中
Waiting 线程状态图![](img\无限等待.png)
### 4.5 补充知识点
到此为止我们已经对线程状态有了基本的认识想要有更多的了解详情可以见下图
![](img\线程状态图.png)
> 一条有意思的tips:
>
> 我们在翻阅API的时候会发现Timed Waiting计时等待 与 Waiting无限等待 状态联系还是很紧密的比如Waiting无限等待 状态中wait方法是空参的而timed waiting计时等待 中wait方法是带参的。这种带参的方法其实是一种倒计时操作相当于我们生活中的小闹钟我们设定好时间到时通知可是如果提前得到唤醒通知那么设定好时间在通知也就显得多此一举了那么这种设计方案其实是一举两得。如果没有得到唤醒通知那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来如果在倒计时期间得到唤醒通知那么线程从Timed Waiting状态立刻唤醒。

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,866 @@
# day07【Lambda、函数式接口、Stream流】
### 主要内容
* Lambda表达式
* 函数式接口
* Stream流
### 教学目标
- [ ] 能够理解函数式编程相对于面向对象的优点
- [ ] 能够掌握Lambda表达式的标准格式
- [ ] 能够掌握Lambda表达式的省略格式与规则
- [ ] 能够使用Consumer&lt;T>函数式接口
- [ ] 能够使用Predicate&lt;T>函数式接口
- [ ] 能够理解流与集合相比的优点
- [ ] 能够掌握常用的流操作
## 第一章 Lambda表达式
### 1.1 函数式编程思想概述
![](img/03-Overview.png)
在数学中,**函数**就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——**强调做什么,而不是以什么形式做**。
**做什么,而不是怎么做**
我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而**不得不**创建一个对象。我们真正希望做的事情是:将`run`方法体内的代码传递给`Thread`类知晓。
**传递一段代码**——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。
### 1.2 Lambda的优化
当需要启动一个线程去完成任务时,通常会通过`java.lang.Runnable`接口来定义任务内容,并使用`java.lang.Thread`类来启动该线程。
**传统写法,代码如下:**
```java
public class Demo03Thread {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start();
}
}
```
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个`Runnable`接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
**代码分析:**
对于`Runnable`的匿名内部类用法,可以分析出几点内容:
- `Thread`类需要`Runnable`接口作为参数,其中的抽象`run`方法是用来指定线程任务内容的核心;
- 为了指定`run`的方法体,**不得不**需要`Runnable`接口的实现类;
- 为了省去定义一个`RunnableImpl`实现类的麻烦,**不得不**使用匿名内部类;
- 必须覆盖重写抽象`run`方法,所以方法名称、方法参数、方法返回值**不得不**再写一遍,且不能写错;
- 而实际上,**似乎只有方法体才是关键所在**。
![](img/02-Lambda.png)
**Lambda表达式写法,代码如下:**
借助Java 8的全新语法上述`Runnable`接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效
```java
public class Demo04LambdaRunnable {
public static void main(String[] args) {
new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
```
这段代码和刚才的执行效果是完全一样的可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!
### 1.3 Lambda的格式
#### 标准格式:
Lambda省去面向对象的条条框框格式由**3个部分**组成:
* 一些参数
* 一个箭头
* 一段代码
Lambda表达式的**标准格式**为:
```
(参数类型 参数名称) -> { 代码语句 }
```
**格式说明:**
* 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
* `->`是新引入的语法格式,代表指向动作。
* 大括号内的语法与传统方法体要求基本一致。
**匿名内部类与lambda对比:**
```java
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start();
```
仔细分析该代码中,`Runnable`接口只有一个`run`方法的定义:
- `public abstract void run();`
即制定了一种做事情的方案(其实就是一个方法):
- **无参数**:不需要任何条件即可执行该方案。
- **无返回值**:该方案不产生任何结果。
- **代码块**(方法体):该方案的具体执行步骤。
同样的语义体现在`Lambda`语法中,要更加简单:
```java
() -> System.out.println("多线程任务执行!")
```
- 前面的一对小括号即`run`方法的参数(无),代表不需要任何条件;
- 中间的一个箭头代表将前面的参数传递给后面的代码;
- 后面的输出语句即业务逻辑代码。
#### 参数和返回值:
下面举例演示`java.util.Comparator&lt;T>`接口的使用场景代码,其中的抽象方法定义为:
* `public abstract int compare(T o1, T o2);`
当需要对一个对象数组进行排序时,`Arrays.sort`方法需要一个`Comparator`接口实例来指定排序的规则。假设有一个`Person`类,含有`String name``int age`两个成员变量:
```java
public class Person {
private String name;
private int age;
// 省略构造器、toString方法与Getter Setter
}
```
**传统写法**
如果使用传统的代码对`Person[]`数组进行排序,写法如下:
```java
public class Demo05Comparator {
public static void main(String[] args) {
// 本来年龄乱序的对象数组
Person[] array = { new Person("古力娜扎", 19), new Person("迪丽热巴", 18), new Person("马尔扎哈", 20) };
// 匿名内部类
Comparator<Person> comp = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
};
Arrays.sort(array, comp); // 第二个参数为排序规则即Comparator接口实例
for (Person person : array) {
System.out.println(person);
}
}
}
```
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中`Comparator`接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。
**代码分析**
下面我们来搞清楚上述代码真正要做什么事情。
- 为了排序,`Arrays.sort`方法需要排序规则,即`Comparator`接口的实例,抽象方法`compare`是关键;
- 为了指定`compare`的方法体,**不得不**需要`Comparator`接口的实现类;
- 为了省去定义一个`ComparatorImpl`实现类的麻烦,**不得不**使用匿名内部类;
- 必须覆盖重写抽象`compare`方法,所以方法名称、方法参数、方法返回值**不得不**再写一遍,且不能写错;
- 实际上,**只有参数和方法体才是关键**。
**Lambda写法**
```java
public class Demo06ComparatorLambda {
public static void main(String[] args) {
Person[] array = {
new Person("古力娜扎", 19),
new Person("迪丽热巴", 18),
new Person("马尔扎哈", 20) };
Arrays.sort(array, (Person a, Person b) -> {
return a.getAge() - b.getAge();
});
for (Person person : array) {
System.out.println(person);
}
}
}
```
#### 省略格式:
**省略规则**
在Lambda标准格式的基础上使用省略写法的规则为
1. 小括号内参数的类型可以省略;
2. 如果小括号内**有且仅有一个参**,则小括号可以省略;
3. 如果大括号内**有且仅有一个语句**则无论是否有返回值都可以省略大括号、return关键字及语句分号。
> 备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。
**可推导即可省略**
Lambda强调的是“做什么”而不是“怎么做”所以凡是可以推导得知的信息都可以省略。例如上例还可以使用Lambda的省略写法
```java
Runnable接口简化:
1. () -> System.out.println("多线程任务执行!")
Comparator接口简化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());
```
### 1.4 Lambda的前提条件
Lambda的语法非常简洁完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意
1. 使用Lambda必须具有接口且要求**接口中有且仅有一个抽象方法**。
无论是JDK内置的`Runnable``Comparator`接口还是自定义的接口只有当接口中的抽象方法存在且唯一时才可以使用Lambda。
2. 使用Lambda必须具有接口作为方法参数。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型才能使用Lambda作为该接口的实例。
> 备注:有且仅有一个抽象方法的接口,称为“**函数式接口**”。
## 第二章 函数式接口
### 2.1 概述
函数式接口在Java中是指**有且仅有一个抽象方法的接口**。
函数式接口即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法Java中的Lambda才能顺利地进行推导。
> 备注:从应用层面来讲Java中的Lambda可以看做是匿名内部类的简化格式但是二者在原理上不同。
#### 格式
只要确保接口中有且仅有一个抽象方法即可:
```java
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}
```
由于接口当中抽象方法的`public abstract`是可以省略的,所以定义一个函数式接口很简单:
```java
public interface MyFunctionalInterface {
void myMethod();
}
```
#### 自定义函数式接口
对于刚刚定义好的`MyFunctionalInterface`函数式接口,典型使用场景就是作为方法的参数:
```java
public class Demo07FunctionalInterface {
// 使用自定义的函数式接口作为方法参数
private static void doSomething(MyFunctionalInterface inter) {
inter.myMethod(); // 调用自定义的函数式接口方法
}
public static void main(String[] args) {
// 调用使用函数式接口的方法
doSomething(() -> System.out.println("Lambda执行啦"));
}
}
```
#### FunctionalInterface注解
`@Override`注解的作用类似Java 8中专门为函数式接口引入了一个新的注解`@FunctionalInterface`。该注解可用于一个接口的定义上:
```java
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
```
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
### 2.2 常用函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景它们主要在`java.util.function`包中被提供。下面是两个常用的函数式接口及使用示例。
#### Consumer接口
`java.util.function.Consumer&lt;T>`接口,是**消费**一个数据,其数据类型由泛型参数决定。
**抽象方法accept**
`Consumer`接口中包含抽象方法`void accept(T t)`: 消费一个指定泛型的数据。
基本使用如:
```java
//给你一个字符串,请按照大写的方式进行消费
import java.util.function.Consumer;
public class Demo08Consumer {
public static void main(String[] args) {
String str = "Hello World";
//1.lambda表达式标准格式
fun(str,(String s)->{
System.out.println(s.toUpperCase());
});
//2.lambda表达式简化格式
fun(str,s-> System.out.println(s.toUpperCase()));
}
/*
定义方法,使用Consumer接口作为参数
fun方法: 消费一个String类型的变量
*/
public static void fun(String s,Consumer<String> con) {
con.accept(s);
}
}
```
#### Predicate接口
有时候我们需要对某种类型的数据进行判断从而得到一个boolean值结果。这时可以使用`java.util.function.Predicate&lt;T>`接口。
**抽象方法test**
`Predicate`接口中包含一个抽象方法:`boolean test(T t)`。用于条件判断的场景:
```java
//1.练习:判断字符串长度是否大于5
//2.练习:判断字符串是否包含"H"
public class Demo09Predicate {
private static void method(Predicate<String> predicate,String str) {
boolean veryLong = predicate.test(str);
System.out.println("字符串很长吗:" + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() > 5, "HelloWorld");
}
}
```
条件判断的标准是传入的Lambda表达式逻辑只要字符串长度大于5则认为很长。
## 第三章 Stream流
在Java 8中得益于Lambda所带来的函数式编程引入了一个**全新的Stream概念**,用于解决已有集合类库既有的弊端。
### 3.1 引言
**传统集合的多步遍历代码**
几乎所有的集合(如`Collection`接口或`Map`接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:
```java
public class Demo10ForEach {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
for (String name : list) {
System.out.println(name);
}
}
}
```
这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。
**循环遍历的弊端**
Java 8的Lambda让我们可以更加专注于**做什么**What而不是**怎么做**How这点此前已经结合内部类进行了对比说明。现在我们仔细体会一下上例代码可以发现
- for循环的语法就是“**怎么做**”
- for循环的循环体才是“**做什么**”
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,**而并不是从第一个到最后一个顺次处理的循环**。前者是目的,后者是方式。
试想一下,如果希望对集合中的元素进行筛选过滤:
1. 将集合A根据条件一过滤为**子集B**
2. 然后再根据条件二过滤为**子集C**。
那怎么办在Java 8之前的做法可能为
这段代码中含有三个循环,每一个作用不同:
1. 首先筛选所有姓张的人;
2. 然后筛选名字有三个字的人;
3. 最后进行对结果进行打印输出。
```java
public class Demo11NormalFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
List<String> zhangList = new ArrayList<>();
for (String name : list) {
if (name.startsWith("张")) {
zhangList.add(name);
}
}
List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
shortList.add(name);
}
}
for (String name : shortList) {
System.out.println(name);
}
}
}
```
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?**不是。**循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。
Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢
**Stream的更优写法**
下面来看一下借助Java 8的Stream API什么才叫优雅
```java
public class Demo12StreamFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(s -> System.out.println(s));
}
}
```
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:**获取流、过滤姓张、过滤长度为3、逐一打印**。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
### 3.2 流式思想概述
**注意请暂时忘记对传统IO流的固有印象**
整体来看,流式思想类似于工厂车间的“**生产流水线**”。
![](img/02-流水线.jpeg)
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。
![](img/01-流式思想示意图.png)
这张图中展示了过滤、映射、跳过、计数等多步操作这是一种集合元素的处理方案而方案就是一种“函数模型”。图中的每一个方框都是一个“流”调用指定的方法可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。
这里的`filter``map``skip`都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法`count`执行的时候整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
> 备注“Stream流”其实是一个集合元素的函数模型它并不是集合也不是数据结构其本身并不存储任何元素或其地址值
### 3.3 获取流方式
`java.util.stream.Stream&lt;T>`是Java 8新加入的最常用的流接口。这并不是一个函数式接口。
获取一个流非常简单,有以下几种常用的方式:
- 所有的`Collection`集合都可以通过`stream`默认方法获取流;
- `Stream`接口的静态方法`of`可以获取数组对应的流。
**方式1 : 根据Collection获取流**
首先,`java.util.Collection`接口中加入了default方法`stream`用来获取流,所以其所有实现类均可获取流。
```java
import java.util.*;
import java.util.stream.Stream;
/*
获取Stream流的方式
1.Collection中 方法
Stream stream()
2.Stream接口 中静态方法
of(T...t) 向Stream中添加多个数据
*/
public class Demo13GetStream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
// ...
Stream<String> stream2 = set.stream();
}
}
```
**方式2: 根据数组获取流**
如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以`Stream`接口中提供了静态方法`of`,使用很简单:
```java
import java.util.stream.Stream;
public class Demo14GetStream {
public static void main(String[] args) {
String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
Stream<String> stream = Stream.of(array);
}
}
```
> 备注:`of`方法的参数其实是一个可变参数,所以支持数组。
### 3.4 常用方法
流模型的操作很丰富这里介绍一些常用的API。这些方法可以被分成两种
- **终结方法**:返回值类型不再是`Stream`接口自身类型的方法,因此不再支持类似`StringBuilder`那样的链式调用。本小节中,终结方法包括`count``forEach`方法。
- **非终结方法**:返回值类型仍然是`Stream`接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
> 备注本小节之外的更多方法请自行参考API文档。
#### forEach : 逐一处理
虽然方法名字叫`forEach`但是与for循环中的“for-each”昵称不同该方法**并不保证元素的逐一消费动作在流中是被有序执行的**。
```java
void forEach(Consumer<? super T> action);
```
该方法接收一个`Consumer`接口函数,会将每一个流元素交给该函数进行处理。例如:
```java
import java.util.stream.Stream;
public class Demo15StreamForEach {
public static void main(String[] args) {
Stream<String> stream = Stream.of("大娃","二娃","三娃","四娃","五娃","六娃","七娃","爷爷","蛇精","蝎子精");
//Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
stream.forEach((String str)->{System.out.println(str);});
}
}
```
在这里lambda表达式`(String str)->&#123;System.out.println(str);}`就是一个Consumer函数式接口的示例。
#### filter过滤
可以通过`filter`方法将一个流转换成另一个子集流。方法声明:
```java
Stream<T> filter(Predicate<? super T> predicate);
```
该接口接收一个`Predicate`函数式接口参数可以是一个Lambda作为筛选条件。
**基本使用**
Stream流中的`filter`方法基本使用的代码如:
```java
public class Demo16StreamFilter {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter((String s) -> {return s.startsWith("张");});
}
}
```
在这里通过Lambda表达式来指定了筛选的条件必须姓张。
#### count统计个数
正如旧集合`Collection`当中的`size`方法一样,流提供`count`方法来数一数其中的元素个数:
```java
long count();
```
该方法返回一个long值代表元素个数不再像旧集合那样是int值。基本使用
```java
public class Demo17StreamCount {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s -> s.startsWith("张"));
System.out.println(result.count()); // 2
}
}
```
#### limit取用前几个
`limit`方法可以对流进行截取只取用前n个。方法签名
```java
Stream<T> limit(long maxSize):获取Stream流对象中的前n个元素,返回一个新的Stream流对象
```
参数是一个long型如果集合当前长度大于参数则进行截取否则不进行操作。基本使用
```java
import java.util.stream.Stream;
public class Demo18StreamLimit {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.limit(2);
System.out.println(result.count()); // 2
}
}
```
#### skip跳过前几个
如果希望跳过前几个元素,可以使用`skip`方法获取一个截取之后的新流:
```java
Stream<T> skip(long n): 跳过Stream流对象中的前n个元素,返回一个新的Stream流对象
```
如果流的当前长度大于n则跳过前n个否则将会得到一个长度为0的空流。基本使用
```java
import java.util.stream.Stream;
public class Demo19StreamSkip {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1
}
}
```
#### concat组合
如果有两个流,希望合并成为一个流,那么可以使用`Stream`接口的静态方法`concat`
```java
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b): 把参数列表中的两个Stream流对象a和b,合并成一个新的Stream流对象
```
> 备注:这是一个静态方法,与`java.lang.String`当中的`concat`方法是不同的。
该方法的基本使用代码如:
```java
import java.util.stream.Stream;
public class Demo20StreamConcat {
public static void main(String[] args) {
Stream<String> streamA = Stream.of("张无忌");
Stream<String> streamB = Stream.of("张翠山");
Stream<String> result = Stream.concat(streamA, streamB);
}
}
```
### 4.5 Stream综合案例
现在有两个`ArrayList`集合存储队伍当中的多个成员姓名要求使用传统的for循环或增强for循环**依次**进行以下若干操作步骤:
1. 第一个队伍只要名字为3个字的成员姓名
2. 第一个队伍筛选之后只要前3个人
3. 第二个队伍只要姓张的成员姓名;
4. 第二个队伍筛选之后不要前2个人
5. 将两个队伍合并为一个队伍;
6. 打印整个队伍的姓名信息。
两个队伍(集合)的代码如下:
```java
public class Demo21ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("宋远桥");
one.add("苏星河");
one.add("老子");
one.add("庄子");
one.add("孙子");
one.add("洪七公");
List<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("张无忌");
two.add("张三丰");
two.add("赵丽颖");
two.add("张二狗");
two.add("张天爱");
two.add("张三");
// ....
}
}
```
**传统方式**
使用for循环 , 示例代码:
```java
public class Demo22ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一个队伍只要名字为3个字的成员姓名
List<String> oneA = new ArrayList<>();
for (String name : one) {
if (name.length() == 3) {
oneA.add(name);
}
}
// 第一个队伍筛选之后只要前3个人
List<String> oneB = new ArrayList<>();
for (int i = 0; i < 3; i++) {
oneB.add(oneA.get(i));
}
// 第二个队伍只要姓张的成员姓名;
List<String> twoA = new ArrayList<>();
for (String name : two) {
if (name.startsWith("张")) {
twoA.add(name);
}
}
// 第二个队伍筛选之后不要前2个人
List<String> twoB = new ArrayList<>();
for (int i = 2; i < twoA.size(); i++) {
twoB.add(twoA.get(i));
}
// 将两个队伍合并为一个队伍;
List<String> totalNames = new ArrayList<>();
totalNames.addAll(oneB);
totalNames.addAll(twoB);
// 打印整个队伍的姓名信息。
for (String name : totalNames) {
System.out.println(name);
}
}
}
```
运行结果为:
```
宋远桥
苏星河
洪七公
张二狗
张天爱
张三
```
**Stream方式**
等效的Stream流式处理代码为
```java
public class Demo23StreamNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一个队伍只要名字为3个字的成员姓名
// 第一个队伍筛选之后只要前3个人
Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);
// 第二个队伍只要姓张的成员姓名;
// 第二个队伍筛选之后不要前2个人
Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);
// 将两个队伍合并为一个队伍;
// 根据姓名创建Person对象
// 打印整个队伍的Person对象信息。
Stream.concat(streamOne, streamTwo).forEach(s->System.out.println(s));
}
}
```
运行效果完全一样:
```
宋远桥
苏星河
洪七公
张二狗
张天爱
张三
```
### 4.6 函数拼接与终结方法[了解]
在上述介绍的各种方法中,凡是返回值仍然为`Stream`接口的为**函数拼接方法**,它们支持链式调用;而返回值不再为`Stream`接口的为**终结方法**,不再支持链式调用。如下表所示:
| 方法名 | 方法作用 | 方法种类 | 是否支持链式调用 |
| ------- | ---------- | -------- | ---------------- |
| count | 统计个数 | 终结 | 否 |
| forEach | 逐一处理 | 终结 | 否 |
| filter | 过滤 | 函数拼接 | 是 |
| limit | 取用前几个 | 函数拼接 | 是 |
| skip | 跳过前几个 | 函数拼接 | 是 |
| concat | 组合 | 函数拼接 | 是 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,463 @@
# day08【File类、递归】
### 主要内容
* File类
* 递归
### 教学目标
- [ ] 能够说出File对象的创建方式
- [ ] 能够说出File类获取名称的方法名称
- [ ] 能够说出File类获取绝对路径的方法名称
- [ ] 能够说出File类获取文件大小的方法名称
- [ ] 能够说出File类判断是否是文件的方法名称
- [ ] 能够说出File类判断是否是文件夹的方法名称
- [ ] 能够辨别相对路径和绝对路径
- [ ] 能够遍历文件夹
- [ ] 能够解释递归的含义
- [ ] 能够使用递归的方式计算5的阶乘
- [ ] 能够说出使用递归会内存溢出隐患的原因
## 第一章 File类
### 1.1 概述
`java.io.File` 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
### 1.2 构造方法
* `public File(String pathname) ` :通过将给定的**路径名字符串**转换为抽象路径名来创建新的 File实例。
* `public File(String parent, String child) ` :从**父路径名字符串和子路径名字符串**创建新的 File实例。
* `public File(File parent, String child)` :从**父抽象路径名和子路径名字符串**创建新的 File实例。
* 构造举例,代码如下:
```java
// 文件路径名
String pathname = "D:\\aaa.txt";
File file1 = new File(pathname);
// 文件路径名
String pathname2 = "D:\\aaa\\bbb.txt";
File file2 = new File(pathname2);
// 通过父路径和子路径字符串
String parent = "d:\\aaa";
String child = "bbb.txt";
File file3 = new File(parent, child);
// 通过父级File对象和子路径字符串
File parentDir = new File("d:\\aaa");
String child = "bbb.txt";
File file4 = new File(parentDir, child);
```
> 小贴士:
>
> 1. 一个File对象代表硬盘中实际存在的一个文件或者目录。
> 2. 无论该路径下是否存在文件或者目录都不影响File对象的创建。
### 1.3 常用方法
#### 获取功能的方法
* `public String getAbsolutePath() ` 返回此File的绝对路径名字符串。
* ` public String getPath() ` 将此File转换为路径名字符串。
* `public String getName()` 返回由此File表示的文件或目录的名称。
* `public long length()` 返回由此File表示的文件的长度。
方法演示,代码如下:
```java
public class FileGet {
public static void main(String[] args) {
File f = new File("d:/aaa/bbb.java");
System.out.println("文件绝对路径:"+f.getAbsolutePath());
System.out.println("文件构造路径:"+f.getPath());
System.out.println("文件名称:"+f.getName());
System.out.println("文件长度:"+f.length()+"字节");
File f2 = new File("d:/aaa");
System.out.println("目录绝对路径:"+f2.getAbsolutePath());
System.out.println("目录构造路径:"+f2.getPath());
System.out.println("目录名称:"+f2.getName());
System.out.println("目录长度:"+f2.length());
}
}
输出结果:
文件绝对路径:d:\aaa\bbb.java
文件构造路径:d:\aaa\bbb.java
文件名称:bbb.java
文件长度:636字节
目录绝对路径:d:\aaa
目录构造路径:d:\aaa
目录名称:aaa
目录长度:4096
```
> API中说明length()表示文件的长度。但是File对象表示目录则返回值未指定。
#### 绝对路径和相对路径
* **绝对路径**:从盘符开始的路径,这是一个完整的路径。
* **相对路径**:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
```java
public class FilePath {
public static void main(String[] args) {
// D盘下的bbb.java文件
File f = new File("D:\\bbb.java");
System.out.println(f.getAbsolutePath());
// 项目下的bbb.java文件
File f2 = new File("bbb.java");
System.out.println(f2.getAbsolutePath());
}
}
输出结果:
D:\bbb.java
D:\idea_project_test4\bbb.java
```
#### 判断功能的方法
- `public boolean exists()` 此File表示的文件或目录是否实际存在。
- `public boolean isDirectory()` 此File表示的是否为目录。
- `public boolean isFile()` 此File表示的是否为文件。
方法演示,代码如下:
```java
public class FileIs {
public static void main(String[] args) {
File f = new File("d:\\aaa\\bbb.java");
File f2 = new File("d:\\aaa");
// 判断是否存在
System.out.println("d:\\aaa\\bbb.java 是否存在:"+f.exists());
System.out.println("d:\\aaa 是否存在:"+f2.exists());
// 判断是文件还是目录
System.out.println("d:\\aaa 文件?:"+f2.isFile());
System.out.println("d:\\aaa 目录?:"+f2.isDirectory());
}
}
输出结果:
d:\aaa\bbb.java 是否存在:true
d:\aaa 是否存在:true
d:\aaa 文件?:false
d:\aaa 目录?:true
```
#### 创建删除功能的方法
- `public boolean createNewFile()` :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
- `public boolean delete()` 删除由此File表示的文件或目录。
- `public boolean mkdir()` 创建由此File表示的目录。
- `public boolean mkdirs()` 创建由此File表示的目录包括任何必需但不存在的父目录。
方法演示,代码如下:
```java
public class FileCreateDelete {
public static void main(String[] args) throws IOException {
// 文件的创建
File f = new File("aaa.txt");
System.out.println("是否存在:"+f.exists()); // false
System.out.println("是否创建:"+f.createNewFile()); // true
System.out.println("是否存在:"+f.exists()); // true
// 目录的创建
File f2= new File("newDir");
System.out.println("是否存在:"+f2.exists());// false
System.out.println("是否创建:"+f2.mkdir()); // true
System.out.println("是否存在:"+f2.exists());// true
// 创建多级目录
File f3= new File("newDira\\newDirb");
System.out.println(f3.mkdir());// false
File f4= new File("newDira\\newDirb");
System.out.println(f4.mkdirs());// true
// 文件的删除
System.out.println(f.delete());// true
// 目录的删除
System.out.println(f2.delete());// true
System.out.println(f4.delete());// false
}
}
```
> API中说明delete方法如果此File表示目录则目录必须为空才能删除。
### 1.4 目录的遍历
* `public String[] list()` 返回一个String数组表示该File目录中的所有子文件或目录。
* `public File[] listFiles()` 返回一个File数组表示该File目录中的所有的子文件或目录。
```java
public class FileFor {
public static void main(String[] args) {
File dir = new File("d:\\java_code");
//获取当前目录下的文件以及文件夹的名称。
String[] names = dir.list();
for(String name : names){
System.out.println(name);
}
//获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息
File[] files = dir.listFiles();
for (File file : files) {
System.out.println(file);
}
}
}
```
> 小贴士:
>
> 调用listFiles方法的File对象表示的必须是实际存在的目录否则返回null无法进行遍历。
## 第二章 递归
### 2.1 概述
* **递归**:指在当前方法内调用自己的这种现象。
```java
public static void main(String[] args) {
System.out.println("main");
main(args);
}
```
### 2.2 递归累和
#### 计算1 ~ n的和
**分析**num的累和 = num + (num-1)的累和,所以可以把累和的操作定义成一个方法,递归调用。
**实现代码**
```java
public class DiGuiDemo {
public static void main(String[] args) {
//计算1~num的和使用递归完成
int num = 5;
// 调用求和的方法
int sum = getSum(num);
// 输出结果
System.out.println(sum);
}
/*
通过递归算法实现.
参数列表:int
返回值类型: int
*/
public static int getSum(int num) {
/*
num为1时,方法返回1,
相当于是方法的出口,num总有是1的情况
*/
if(num == 1){
return 1;
}
/*
num不为1时,方法返回 num +(num-1)的累和
递归调用getSum方法
*/
return num + getSum(num-1);
}
}
```
#### 代码执行图解
![](img/day08_01_递归累和.jpg)
> 小贴士:递归一定要有条件限定,保证递归能够停止下来,次数不要太多,否则会发生栈内存溢出。
### 2.3 递归求阶乘
* **阶乘**:所有小于及等于该数的正整数的积。
```java
n的阶乘n! = n * (n-1) *...* 3 * 2 * 1
```
**分析**:这与累和类似,只不过换成了乘法运算学员可以自己练习需要注意阶乘值符合int类型的范围。
```
推理得出n! = n * (n-1)!
```
**代码实现**
```java
public class DiGuiDemo {
//计算n的阶乘使用递归完成
public static void main(String[] args) {
int n = 3;
// 调用求阶乘的方法
int value = getValue(n);
// 输出结果
System.out.println("阶乘为:"+ value);
}
/*
通过递归算法实现.
参数列表:int
返回值类型: int
*/
public static int getValue(int n) {
// 1的阶乘为1
if (n == 1) {
return 1;
}
/*
n不为1时,方法返回 n! = n*(n-1)!
递归调用getValue方法
*/
return n * getValue(n - 1);
}
}
```
### 2.4 递归打印多级目录
**分析**:多级目录的打印,就是当目录的嵌套。遍历之前,无从知道到底有多少级目录,所以我们还是要使用递归实现。
**代码实现**
```java
public class DiGuiDemo2 {
public static void main(String[] args) {
// 创建File对象
File dir = new File("D:\\aaa");
// 调用打印目录方法
printDir(dir);
}
public static void printDir(File dir) {
// 获取子文件和目录
File[] files = dir.listFiles();
// 循环打印
/*
判断:
当是文件时,打印绝对路径.
当是目录时,继续调用打印目录的方法,形成递归调用.
*/
for (File file : files) {
// 判断
if (file.isFile()) {
// 是文件,输出文件绝对路径
System.out.println("文件名:"+ file.getAbsolutePath());
} else {
// 是目录,输出目录绝对路径
System.out.println("目录:"+file.getAbsolutePath());
// 继续遍历,调用printDir,形成递归
printDir(file);
}
}
}
}
```
## 第三章 综合案例
### 3.1 文件搜索
搜索`D:\aaa` 目录中的`.java` 文件。
**分析**
1. 目录搜索,无法判断多少级目录,所以使用递归,遍历所有目录。
2. 遍历目录时,获取的子文件,通过文件名称,判断是否符合条件。
**代码实现**
```java
public class DiGuiDemo3 {
public static void main(String[] args) {
// 创建File对象
File dir = new File("D:\\aaa");
// 调用打印目录方法
printDir(dir);
}
public static void printDir(File dir) {
// 获取子文件和目录
File[] files = dir.listFiles();
// 循环打印
for (File file : files) {
if (file.isFile()) {
// 是文件,判断文件名并输出文件绝对路径
if (file.getName().endsWith(".java")) {
System.out.println("文件名:" + file.getAbsolutePath());
}
} else {
// 是目录,继续遍历,形成递归
printDir(file);
}
}
}
}
```
### 3.2 文件过滤器优化
`java.io.FileFilter`是一个接口是File的过滤器。 该接口的对象可以传递给File类的`listFiles(FileFilter)` 作为参数, 接口中只有一个方法。
`boolean accept(File pathname) ` 测试pathname是否应该包含在当前File目录中符合则返回true。
**分析**
1. 接口作为参数,需要传递子类对象,重写其中方法。我们选择匿名内部类方式,比较简单。
2. `accept`方法参数为File表示当前File下所有的子文件和子目录。保留住则返回true过滤掉则返回false。保留规则
1. 要么是.java文件。
2. 要么是目录,用于继续遍历。
3. 通过过滤器的作用,`listFiles(FileFilter)`返回的数组元素中,子文件对象都是符合条件的,可以直接打印。
**代码实现:**
```java
public class DiGuiDemo4 {
public static void main(String[] args) {
File dir = new File("D:\\aaa");
printDir2(dir);
}
public static void printDir2(File dir) {
// 匿名内部类方式,创建过滤器子类对象
File[] files = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".java")||pathname.isDirectory();
}
});
// 循环打印
for (File file : files) {
if (file.isFile()) {
System.out.println("文件名:" + file.getAbsolutePath());
} else {
printDir2(file);
}
}
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,939 @@
# day09【字节流、字符流】
### 主要内容
* IO流
* 字节流
* 字符流
* 异常处理
* Properties
### 教学目标
- [ ] 能够说出IO流的分类和功能
- [ ] 能够使用字节输出流写出数据到文件
- [ ] 能够使用字节输入流读取数据到程序
- [ ] 能够理解读取数据read(byte[])方法的原理
- [ ] 能够使用字节流完成文件的复制
- [ ] 能够使用FileWirter写数据到文件
- [ ] 能够说出FileWriter中关闭和刷新方法的区别
- [ ] 能够使用FileWriter写数据的5个方法
- [ ] 能够使用FileWriter写数据实现换行和追加写
- [ ] 能够使用FileReader读数据
- [ ] 能够使用FileReader读数据一次一个字符数组
- [ ] 能够使用Properties的load方法加载文件中配置信息
## 第一章 IO概述
### 1.1 什么是IO
生活中,你肯定经历过这样的场景。当你编辑一个文本文件,忘记了`ctrl+s` 可能文件就白白编辑了。当你电脑上插入一个U盘可以把一个视频拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢键盘、内存、硬盘、外接设备等等。
我们把这种数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为`输入input``输出output` ,即流向内存是输入流,流出内存的输出流。
Java中I/O操作主要是指使用`java.io`包下的内容,进行输入、输出操作。**输入**也叫做**读取**数据,**输出**也叫做作**写出**数据。
### 1.2 IO的分类
根据数据的流向分为:**输入流**和**输出流**。
* **输入流** :把数据从`其他设备`上读取到`内存`中的流。
* **输出流** :把数据从`内存` 中写出到`其他设备`上的流。
格局数据的类型分为:**字节流**和**字符流**。
* **字节流** :以字节为单位,读写数据的流。
* **字符流** :以字符为单位,读写数据的流。
### 1.3 IO的流向说明图解
![](img/1_io.jpg)
### 1.4 顶级父类们
| | **输入流** | 输出流 |
| :-----: | :------------------------: | :-------------------------: |
| **字节流** | 字节输入流<br />**InputStream** | 字节输出流<br />**OutputStream** |
| **字符流** | 字符输入流<br />**Reader** | 字符输出流<br />**Writer** |
## 第二章 字节流
### 2.1 一切皆为字节
一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
### 2.2 字节输出流【OutputStream】
`java.io.OutputStream `抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
* `public void close()` :关闭此输出流并释放与此流相关联的任何系统资源。
* `public void flush() ` :刷新此输出流并强制任何缓冲的输出字节被写出。
* `public void write(byte[] b)`:将 b.length字节从指定的字节数组写入此输出流。
* `public void write(byte[] b, int off, int len)` :从指定的字节数组写入 len字节从偏移量 off开始输出到此输出流。
* `public abstract void write(int b)` :将指定的字节输出流。
> 小贴士:
>
> close方法当完成流的操作时必须调用此方法释放系统资源。
### 2.3 FileOutputStream类
`OutputStream`有很多子类,我们从最简单的一个子类开始。
`java.io.FileOutputStream `类是文件输出流,用于将数据写出到文件。
#### 构造方法
* `public FileOutputStream(File file)`:创建文件输出流以写入由指定的 File对象表示的文件。
* `public FileOutputStream(String name)` 创建文件输出流以指定的名称写入文件。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。
* 构造举例,代码如下:
```java
public class FileOutputStreamConstructor throws IOException {
public static void main(String[] args) {
// 使用File对象创建流对象
File file = new File("a.txt");
FileOutputStream fos = new FileOutputStream(file);
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("b.txt");
}
}
```
#### 写出字节数据
1. **写出字节**`write(int b)` 方法,每次可以写出一个字节数据,代码使用演示:
```java
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 写出数据
fos.write(97); // 写出第1个字节
fos.write(98); // 写出第2个字节
fos.write(99); // 写出第3个字节
// 关闭资源
fos.close();
}
}
输出结果
abc
```
> 小贴士:
>
> 1. 虽然参数为int类型四个字节但是只会保留一个字节的信息写出。
> 2. 流操作完毕后必须释放系统资源调用close方法千万记得。
2. **写出字节数组**`write(byte[] b)`,每次可以写出数组中的数据,代码使用演示:
```java
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 字符串转换为字节数组
byte[] b = "java程序员".getBytes();
// 写出字节数组数据
fos.write(b);
// 关闭资源
fos.close();
}
}
输出结果
java程序员
```
3. **写出指定长度字节数组**`write(byte[] b, int off, int len)` ,每次写出从off索引开始len个字节代码使用演示
```java
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 写出从索引2开始2个字节。索引2是c两个字节也就是cd。
fos.write(b,2,2);
// 关闭资源
fos.close();
}
}
输出结果
cd
```
#### 数据追加续写
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?
- `public FileOutputStream(File file, boolean append)` 创建文件输出流以写入由指定的 File对象表示的文件。
- `public FileOutputStream(String name, boolean append)` 创建文件输出流以指定的名称写入文件。
这两个构造方法参数中都需要传入一个boolean类型的值`true` 表示追加数据,`false` 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,代码使用演示:
```java
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt"true);
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 写出从索引2开始2个字节。索引2是c两个字节也就是cd。
fos.write(b);
// 关闭资源
fos.close();
}
}
文件操作前cd
文件操作后cdabcde
```
#### 写出换行
Windows系统里换行符号是`\r\n` 。把
以指定是否追加续写了,代码使用演示:
```java
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 定义字节数组
byte[] words = {97,98,99,100,101};
// 遍历数组
for (int i = 0; i < words.length; i++) {
// 写出一个字节
fos.write(words[i]);
// 写出一个换行, 换行符号转成数组写出
fos.write("\r\n".getBytes());
}
// 关闭资源
fos.close();
}
}
输出结果
a
b
c
d
e
```
> * 回车符`\r`和换行符`\n`
> * 回车符回到一行的开头return
> * 换行符下一行newline
> * 系统中的换行:
> * Windows系统里每行结尾是 `回车+换行` ,即`\r\n`
> * Unix系统里每行结尾只有 `换行` ,即`\n`
> * Mac系统里每行结尾是 `回车` ,即`\r`。从 Mac OS X开始与Linux统一。
### 2.4 字节输入流【InputStream】
`java.io.InputStream `抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
- `public void close()` :关闭此输入流并释放与此流相关联的任何系统资源。
- `public abstract int read()` 从输入流读取数据的下一个字节。
- `public int read(byte[] b)` 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。
> 小贴士:
>
> close方法当完成流的操作时必须调用此方法释放系统资源。
### 2.5 FileInputStream类
`java.io.FileInputStream `类是文件输入流,从文件中读取字节。
#### 构造方法
* `FileInputStream(File file)` 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。
* `FileInputStream(String name)` 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出`FileNotFoundException`
- 构造举例,代码如下:
```java
public class FileInputStreamConstructor throws IOException{
public static void main(String[] args) {
// 使用File对象创建流对象
File file = new File("a.txt");
FileInputStream fos = new FileInputStream(file);
// 使用文件名称创建流对象
FileInputStream fos = new FileInputStream("b.txt");
}
}
```
#### 读取字节数据
1. **读取字节**`read`方法每次可以读取一个字节的数据提升为int类型读取到文件末尾返回`-1`,代码使用演示:
```java
public class FISRead {
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象
FileInputStream fis = new FileInputStream("read.txt");
// 读取数据,返回一个字节
int read = fis.read();
System.out.println((char) read);
read = fis.read();
System.out.println((char) read);
read = fis.read();
System.out.println((char) read);
read = fis.read();
System.out.println((char) read);
read = fis.read();
System.out.println((char) read);
// 读取到末尾,返回-1
read = fis.read();
System.out.println( read);
// 关闭资源
fis.close();
}
}
输出结果
a
b
c
d
e
-1
```
循环改进读取方式,代码使用演示:
```java
public class FISRead {
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象
FileInputStream fis = new FileInputStream("read.txt");
// 定义变量,保存数据
int b
// 循环读取
while ((b = fis.read())!=-1) {
System.out.println((char)b);
}
// 关闭资源
fis.close();
}
}
输出结果
a
b
c
d
e
```
> 小贴士:
>
> 1. 虽然读取了一个字节但是会自动提升为int类型。
> 2. 流操作完毕后必须释放系统资源调用close方法千万记得。
2. **使用字节数组读取**`read(byte[] b)`每次读取b的长度个字节到数组中返回读取到的有效字节个数读取到末尾时返回`-1` ,代码使用演示:
```java
public class FISRead {
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象.
FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde
// 定义变量,作为有效个数
int len
// 定义字节数组,作为装字节数据的容器
byte[] b = new byte[2];
// 循环读取
while (( len= fis.read(b))!=-1) {
// 每次读取后,把数组变成字符串打印
System.out.println(new String(b));
}
// 关闭资源
fis.close();
}
}
输出结果
ab
cd
ed
```
错误数据`d`,是由于最后一次读取时,只读取一个字节`e`,数组中,上次读取的数据没有被完全替换,所以要通过`len` ,获取有效的字节,代码使用演示:
```java
public class FISRead {
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象.
FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde
// 定义变量,作为有效个数
int len
// 定义字节数组,作为装字节数据的容器
byte[] b = new byte[2];
// 循环读取
while (( len= fis.read(b))!=-1) {
// 每次读取后,把数组的有效字节部分,变成字符串打印
System.out.println(new String(b0len));// len 每次读取的有效字节个数
}
// 关闭资源
fis.close();
}
}
输出结果
ab
cd
e
```
> 小贴士:
>
> 使用数组读取每次读取多个字节减少了系统间的IO操作次数从而提高了读写的效率建议开发中使用。
### 2.6 字节流练习:图片复制
#### 复制原理图解
![](img/2_copy.jpg)
#### 案例实现
复制图片文件,代码使用演示:
```java
public class Copy {
public static void main(String[] args) throws IOException {
// 1.创建流对象
// 1.1 指定数据源
FileInputStream fis = new FileInputStream("D:\\test.jpg");
// 1.2 指定目的地
FileOutputStream fos = new FileOutputStream("test_copy.jpg");
// 2.读写数据
// 2.1 定义数组
byte[] b = new byte[1024];
// 2.2 定义长度
int len;
// 2.3 循环读取
while ((len = fis.read(b))!=-1) {
// 2.4 写出数据
fos.write(b, 0 , len);
}
// 3.关闭资源
fos.close();
fis.close();
}
}
```
> 小贴士:
>
> 流的关闭原则:先开后关,后开先关。
## 第三章 字符流
当使用字节流读取文本文件时可能会有一个小问题。就是遇到中文字符时可能不会显示完整的字符那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类以字符为单位读写数据专门用于处理文本文件。
### 3.1 字符输入流【Reader】
`java.io.Reader`抽象类是表示用于读取字符流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
- `public void close()` :关闭此流并释放与此流相关联的任何系统资源。
- `public int read()` 从输入流读取一个字符。
- `public int read(char[] cbuf)` 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。
### 3.2 FileReader类
`java.io.FileReader `类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
> 小贴士:
>
> 1. 字符编码字节与字符的对应规则。Windows系统的中文编码默认是GBK编码表。
>
> idea中UTF-8
>
> 2. 字节缓冲区:一个字节数组,用来临时存储字节数据。
#### 构造方法
- `FileReader(File file)` 创建一个新的 FileReader 给定要读取的File对象。
- `FileReader(String fileName)` 创建一个新的 FileReader ,给定要读取的文件的名称。
当你创建一个流对象时必须传入一个文件路径。类似于FileInputStream 。
- 构造举例,代码如下:
```java
public class FileReaderConstructor throws IOException{
public static void main(String[] args) {
// 使用File对象创建流对象
File file = new File("a.txt");
FileReader fr = new FileReader(file);
// 使用文件名称创建流对象
FileReader fr = new FileReader("b.txt");
}
}
```
#### 读取字符数据
1. **读取字符**`read`方法每次可以读取一个字符的数据提升为int类型读取到文件末尾返回`-1`,循环读取,代码使用演示:
```java
public class FRRead {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileReader fr = new FileReader("read.txt");
// 定义变量,保存数据
int b
// 循环读取
while ((b = fr.read())!=-1) {
System.out.println((char)b);
}
// 关闭资源
fr.close();
}
}
输出结果
```
> 小贴士虽然读取了一个字符但是会自动提升为int类型。
>
2. **使用字符数组读取**`read(char[] cbuf)`每次读取b的长度个字符到数组中返回读取到的有效字符个数读取到末尾时返回`-1` ,代码使用演示:
```java
public class FRRead {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileReader fr = new FileReader("read.txt");
// 定义变量,保存有效字符个数
int len
// 定义字符数组,作为装字符数据的容器
char[] cbuf = new char[2];
// 循环读取
while ((len = fr.read(cbuf))!=-1) {
System.out.println(new String(cbuf));
}
// 关闭资源
fr.close();
}
}
输出结果
黑马
程序
员序
```
获取有效的字符改进,代码使用演示:
```java
public class FISRead {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileReader fr = new FileReader("read.txt");
// 定义变量,保存有效字符个数
int len
// 定义字符数组,作为装字符数据的容器
char[] cbuf = new char[2];
// 循环读取
while ((len = fr.read(cbuf))!=-1) {
System.out.println(new String(cbuf,0,len));
}
// 关闭资源
fr.close();
}
}
输出结果
黑马
程序
```
### 3.3 字符输出流【Writer】
`java.io.Writer `抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
- `public abstract void close()` :关闭此输出流并释放与此流相关联的任何系统资源。
- `public abstract void flush() ` :刷新此输出流并强制任何缓冲的输出字符被写出。
- `public void write(int c)` :写出一个字符。
- `public void write(char[] cbuf)`:将 b.length字符从指定的字符数组写出此输出流。
- `public abstract void write(char[] b, int off, int len)` :从指定的字符数组写出 len字符从偏移量 off开始输出到此输出流。
- `public void write(String str)` :写出一个字符串。
### 3.4 FileWriter类
`java.io.FileWriter `类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
#### 构造方法
- `FileWriter(File file)` 创建一个新的 FileWriter给定要读取的File对象。
- `FileWriter(String fileName)` 创建一个新的 FileWriter给定要读取的文件的名称。
当你创建一个流对象时必须传入一个文件路径类似于FileOutputStream。
- 构造举例,代码如下:
```java
public class FileWriterConstructor {
public static void main(String[] args) throws IOException {
// 使用File对象创建流对象
File file = new File("a.txt");
FileWriter fw = new FileWriter(file);
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("b.txt");
}
}
```
#### 基本写出数据
**写出字符**`write(int b)` 方法,每次可以写出一个字符数据,代码使用演示:
```java
public class FWWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 写出数据
fw.write(97); // 写出第1个字符
fw.write('b'); // 写出第2个字符
fw.write('C'); // 写出第3个字符
fw.write(30000); // 写出第4个字符中文编码表中30000对应一个汉字。
/*
【注意】关闭资源时,与FileOutputStream不同。
如果不关闭,数据只是保存到缓冲区,并未保存到文件。
*/
// fw.close();
}
}
输出结果
abC田
```
> 小贴士:
>
> 1. 虽然参数为int类型四个字节但是只会保留一个字符的信息写出。
> 2. 未调用close方法数据只是保存到了缓冲区并未写出到文件中。
#### 关闭和刷新
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要`flush` 方法了。
* `flush` :刷新缓冲区,流对象可以继续使用。
* `close` :关闭流,释放系统资源。关闭前会刷新缓冲区。
代码使用演示:
```java
public class FWWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 写出数据通过flush
fw.write('刷'); // 写出第1个字符
fw.flush();
fw.write('新'); // 继续写出第2个字符写出成功
fw.flush();
// 写出数据通过close
fw.write('关'); // 写出第1个字符
fw.close();
fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
fw.close();
}
}
```
> 小贴士即便是flush方法写出了数据操作的最后还是要调用close方法释放系统资源。
#### 写出其他数据
1. **写出字符数组** `write(char[] cbuf)``write(char[] cbuf, int off, int len)` 每次可以写出字符数组中的数据用法类似FileOutputStream代码使用演示
```java
public class FWWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 字符串转换为字节数组
char[] chars = "java程序员".toCharArray();
// 写出字符数组
fw.write(chars); // java程序员
// 写出从索引2开始2个字节。索引2是'程',两个字节,也就是'程序'。
fw.write(b,2,2); // 程序
// 关闭资源
fos.close();
}
}
```
2. **写出字符串**`write(String str)``write(String str, int off, int len)` ,每次可以写出字符串中的数据,更为方便,代码使用演示:
```java
public class FWWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 字符串
String msg = "java程序员";
// 写出字符数组
fw.write(msg); //java程序员
// 写出从索引2开始2个字节。索引2是'程',两个字节,也就是'程序'。
fw.write(msg,2,2); // 程序
// 关闭资源
fos.close();
}
}
```
3. **续写和换行**操作类似于FileOutputStream。
```java
public class FWWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象,可以续写数据
FileWriter fw = new FileWriter("fw.txt"true);
// 写出字符串
fw.write("黑马");
// 写出换行
fw.write("\r\n");
// 写出字符串
fw.write("程序员");
// 关闭资源
fw.close();
}
}
输出结果:
黑马
程序员
```
> 小贴士:字符流,只能操作文本文件,不能操作图片,视频等非文本文件。
## 第四章 IO异常的处理
#### JDK7前处理
之前的入门练习,我们一直把异常抛出,而实际开发中并不能这样处理,建议使用`try...catch...finally` 代码块,处理异常部分,代码使用演示:
```java
public class HandleException1 {
public static void main(String[] args) {
// 声明变量
FileWriter fw = null;
try {
//创建流对象
fw = new FileWriter("fw.txt");
// 写出数据
fw.write("java程序员"); //java程序员
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
```
#### JDK7的处理
还可以使用JDK7优化后的`try-with-resource` 语句该语句确保了每个资源在语句结束时关闭。所谓的资源resource是指在程序完成后必须关闭的对象。
格式:
```java
try (创建流对象语句,如果多个,使用';'隔开) {
// 读写数据
} catch (IOException e) {
e.printStackTrace();
}
```
代码使用演示:
```java
public class HandleException2 {
public static void main(String[] args) {
// 创建流对象
try ( FileWriter fw = new FileWriter("fw.txt"); ) {
// 写出数据
fw.write("java程序员"); //java程序员
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
#### JDK9的改进(了解内容)
JDK9中`try-with-resource` 的改进,对于**引入对象**的方式支持的更加简洁。被引入的对象同样可以自动关闭无需手动close我们来了解一下格式。
改进前格式:
```java
// 被final修饰的对象
final Resource resource1 = new Resource("resource1");
// 普通对象
Resource resource2 = new Resource("resource2");
// 引入方式:创建新的变量保存
try (Resource r1 = resource1;
Resource r2 = resource2) {
// 使用对象
}
```
改进后格式:
```java
// 被final修饰的对象
final Resource resource1 = new Resource("resource1");
// 普通对象
Resource resource2 = new Resource("resource2");
// 引入方式:直接引入
try (resource1; resource2) {
// 使用对象
}
```
改进后,代码使用演示:
```java
public class TryDemo {
public static void main(String[] args) throws IOException {
// 创建流对象
final FileReader fr = new FileReader("in.txt");
FileWriter fw = new FileWriter("out.txt");
// 引入到try中
try (fr; fw) {
// 定义变量
int b;
// 读取数据
while ((b = fr.read())!=-1) {
// 写出数据
fw.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
## 第五章 属性集
### 5.1 概述
`java.util.Properties ` 继承于` Hashtable` 来表示一个持久的属性集。它使用键值结构存储数据每个键及其对应值都是一个字符串。该类也被许多Java类使用比如获取系统属性时`System.getProperties` 方法就是返回一个`Properties`对象。
### 5.2 Properties类
#### 构造方法
- `public Properties()` :创建一个空的属性列表。
#### 基本的存储方法
- `public Object setProperty(String key, String value)` 保存一对属性。
- `public String getProperty(String key) ` :使用此属性列表中指定的键搜索属性值。
- `public Set<String> stringPropertyNames() ` :所有键的名称的集合。
```java
public class ProDemo {
public static void main(String[] args) throws FileNotFoundException {
// 创建属性集对象
Properties properties = new Properties();
// 添加键值对元素
properties.setProperty("filename", "a.txt");
properties.setProperty("length", "209385038");
properties.setProperty("location", "D:\\a.txt");
// 打印属性集对象
System.out.println(properties);
// 通过键,获取属性值
System.out.println(properties.getProperty("filename"));
System.out.println(properties.getProperty("length"));
System.out.println(properties.getProperty("location"));
// 遍历属性集,获取所有键的集合
Set<String> strings = properties.stringPropertyNames();
// 打印键值对
for (String key : strings ) {
System.out.println(key+" -- "+properties.getProperty(key));
}
}
}
输出结果:
{filename=a.txt, length=209385038, location=D:\a.txt}
a.txt
209385038
D:\a.txt
filename -- a.txt
length -- 209385038
location -- D:\a.txt
```
#### 与流相关的方法
- `public void load(InputStream inStream)` 从字节输入流中读取键值对。
参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:
```
filename=a.txt
length=209385038
location=D:\a.txt
```
加载代码演示:
```java
public class ProDemo2 {
public static void main(String[] args) throws FileNotFoundException {
// 创建属性集对象
Properties pro = new Properties();
// 加载文本中信息到属性集
pro.load(new FileInputStream("read.txt"));
// 遍历集合并打印
Set<String> strings = pro.stringPropertyNames();
for (String key : strings ) {
System.out.println(key+" -- "+pro.getProperty(key));
}
}
}
输出结果:
filename -- a.txt
length -- 209385038
location -- D:\a.txt
```
> 小贴士:文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -0,0 +1,696 @@
# day10【缓冲流、转换流、序列化流】
### 主要内容
* 转换流
* 缓冲流
* 序列化流
* 打印流
### 教学目标
- [ ] 能够使用字节缓冲流读取数据到程序
- [ ] 能够使用字节缓冲流写出数据到文件
- [ ] 能够明确字符缓冲流的作用和基本用法
- [ ] 能够使用缓冲流的特殊功能
- [ ] 能够阐述编码表的意义
- [ ] 能够使用转换流读取指定编码的文本文件
- [ ] 能够使用转换流写入指定编码的文本文件
- [ ] 能够说出打印流的特点
- [ ] 能够使用序列化流写出对象到文件
- [ ] 能够使用反序列化流读取文件到程序中
## 第一章 缓冲流
昨天学习了基本的一些流作为IO流的入门今天我们要见识一些更强大的流。比如能够高效读写的缓冲流能够转换编码的转换流能够持久化存储对象的序列化流等等。这些功能更为强大的流都是在基本的流对象基础之上创建而来的就像穿上铠甲的武士一样相当于是对基本流对象的一种增强。
### 1.1 概述
缓冲流,也叫高效流是对4个基本的`FileXxx` 流的增强所以也是4个流按照数据类型分类
* **字节缓冲流**`BufferedInputStream``BufferedOutputStream`
* **字符缓冲流**`BufferedReader``BufferedWriter`
缓冲流的基本原理是在创建流对象时会创建一个内置的默认大小的缓冲区数组通过缓冲区读写减少系统IO次数从而提高读写的效率。
### 1.2 字节缓冲流
#### 构造方法
* `public BufferedInputStream(InputStream in)` :创建一个 新的缓冲输入流。
* `public BufferedOutputStream(OutputStream out)` 创建一个新的缓冲输出流。
构造举例,代码如下:
```java
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));
```
#### 效率测试
查询API缓冲流读写方法与基本的流是一致的我们通过复制大文件375MB测试它的效率。
1. 基本流,代码如下:
```java
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
FileInputStream fis = new FileInputStream("jdk9.exe");
FileOutputStream fos = new FileOutputStream("copy.exe")
){
// 读写数据
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("普通流复制时间:"+(end - start)+" 毫秒");
}
}
十几分钟过去了...
```
2. 缓冲流,代码如下:
```java
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("jdk9.exe"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.exe"));
){
// 读写数据
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流复制时间:"+(end - start)+" 毫秒");
}
}
缓冲流复制时间:8016 毫秒
```
如何更快呢?
使用数组的方式,代码如下:
```java
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("jdk9.exe"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.exe"));
){
// 读写数据
int len;
byte[] bytes = new byte[8*1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0 , len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒");
}
}
缓冲流使用数组复制时间:666 毫秒
```
### 1.3 字符缓冲流
#### 构造方法
* `public BufferedReader(Reader in)` :创建一个 新的缓冲输入流。
* `public BufferedWriter(Writer out)` 创建一个新的缓冲输出流。
构造举例,代码如下:
```java
// 创建字节缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字节缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
```
#### 特有方法
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。
* BufferedReader`public String readLine()`: 读一行文字。
* BufferedWriter`public void newLine()`: 写一行行分隔符,由系统属性定义符号。
`readLine`方法演示,代码如下:
```java
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("in.txt"));
// 定义字符串,保存读取的一行文字
String line = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.print(line);
System.out.println("------");
}
// 释放资源
br.close();
}
}
```
`newLine`方法演示,代码如下:
```java
public class BufferedWriterDemo throws IOException {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
// 写出数据
bw.write("黑马");
// 写出换行
bw.newLine();
bw.write("程序");
bw.newLine();
bw.write("员");
bw.newLine();
// 释放资源
bw.close();
}
}
输出效果:
黑马
程序
```
### 1.4 练习:文本排序
请将文本信息恢复顺序。
```
3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
```
#### 案例分析
1. 逐行读取文本信息。
2. 解析文本信息到集合中。
3. 遍历集合,按顺序,写出文本信息。
#### 案例实现
```java
public class BufferedTest {
public static void main(String[] args) throws IOException {
// 创建map集合,保存文本数据,键为序号,值为文字
HashMap<String, String> lineMap = new HashMap<>();
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("in.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
// 读取数据
String line = null;
while ((line = br.readLine())!=null) {
// 解析文本
String[] split = line.split("\\.");
// 保存到集合
lineMap.put(split[0],split[1]);
}
// 释放资源
br.close();
// 遍历map集合
for (int i = 1; i <= lineMap.size(); i++) {
String key = String.valueOf(i);
// 获取map中文本
String value = lineMap.get(key);
// 写出拼接文本
bw.write(key+"."+value);
// 写出换行
bw.newLine();
}
// 释放资源
bw.close();
}
}
```
## 第二章 转换流
### 2.1 字符编码和字符集
#### 字符编码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为**编码** 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为**解码** 。比如说按照A规则存储同样按照A规则解析那么就能显示正确的文本f符号。反之按照A规则存储再按照B规则解析就会导致乱码现象。
* **字符编码`Character Encoding`** : 就是一套自然语言的字符与二进制数之间的对应规则。
#### 字符集
* **字符集 `Charset`**:是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。
计算机要准确的存储和识别各种字符集符号需要进行字符编码一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。![](img/1_charset.jpg)
可见,当指定了**编码**,它所对应的**字符集**自然就指定了,所以**编码**才是我们最终要关心的。
* **ASCII字符集**
* ASCIIAmerican Standard Code for Information Interchange美国信息交换标准代码是基于拉丁字母的一套电脑编码系统用于显示现代英语主要包括控制字符回车键、退格、换行键等和可显示字符英文大小写字符、阿拉伯数字和西文符号
* 基本的ASCII字符集使用7位bits表示一个字符共128字符。ASCII的扩展字符集使用8位bits表示一个字符共256字符方便支持欧洲常用字符。
* **ISO-8859-1字符集**
* 拉丁码表别名Latin-1用于显示欧洲使用的语言包括荷兰、丹麦、德语、意大利语、西班牙语等。
* ISO-5559-1使用单字节编码兼容ASCII编码。
* **GBxxx字符集**
* GB就是国标的意思是为了显示中文而设计的一套字符集。
* **GB2312**简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时就表示一个汉字这样大约可以组合了包含7000多个简体汉字此外数学符号、罗马希腊的字母、日文的假名们都编进去了连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码这就是常说的"全角"字符而原来在127号以下的那些就叫"半角"字符了。
* **GBK**最常用的中文码表。是在GB2312标准基础上的扩展规范使用了双字节编码方案共收录了21003个汉字完全兼容GB2312标准同时支持繁体汉字以及日韩汉字等。
* **GB18030**最新的中文码表。收录汉字70244个采用多字节编码每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字同时支持繁体汉字以及日韩汉字等。
* **Unicode字符集**
* Unicode编码系统为表达任意语言的任意字符而设计是业界的一种标准也称为统一码、标准万国码。
* 它最多使用4个字节的数字来表达每个字母、符号或者文字。有三种编码方案UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。
* UTF-8编码可以用来表示Unicode标准中任何字符它是电子邮件、网页及其他存储或传送文字的应用中优先采用的编码。互联网工程工作小组IETF要求所有互联网协议都必须支持UTF-8编码。所以我们开发Web应用也要使用UTF-8编码。它使用一至四个字节为每个字符编码编码规则
1. 128个US-ASCII字符只需一个字节编码。
2. 拉丁文等字符,需要二个字节编码。
3. 大部分常用字(含中文),使用三个字节编码。
4. 其他极少使用的Unicode辅助字符使用四字节编码。
### 2.2 编码引出的问题
在IDEA中使用`FileReader` 读取项目中的文本文件。由于IDEA的设置都是默认的`UTF-8`编码所以没有任何问题。但是当读取Windows系统中创建的文本文件时由于Windows系统的默认是GBK编码就会出现乱码。
```java
public class ReaderDemo {
public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader("E:\\File_GBK.txt");
int read;
while ((read = fileReader.read()) != -1) {
System.out.print((char)read);
}
fileReader.close();
}
}
输出结果:
<EFBFBD><EFBFBD><EFBFBD>
```
那么如何读取GBK编码的文件呢
### 2.3 InputStreamReader类
转换流`java.io.InputStreamReader`是Reader的子类是从字节流到字符流的桥梁。它读取字节并使用指定的字符集将其解码为字符。它的字符集可以由名称指定也可以接受平台的默认字符集。
#### 构造方法
* `InputStreamReader(InputStream in)`: 创建一个使用默认字符集的字符流。
* `InputStreamReader(InputStream in, String charsetName)`: 创建一个指定字符集的字符流。
构造举例,代码如下:
```java
InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");
```
#### 指定编码读取
```java
public class ReaderDemo2 {
public static void main(String[] args) throws IOException {
// 定义文件路径,文件为gbk编码
String FileName = "E:\\file_gbk.txt";
// 创建流对象,默认UTF8编码
InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName));
// 创建流对象,指定GBK编码
InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , "GBK");
// 定义变量,保存字符
int read;
// 使用默认编码字符流读取,乱码
while ((read = isr.read()) != -1) {
System.out.print((char)read); // <20><>Һ<EFBFBD>
}
isr.close();
// 使用指定编码字符流读取,正常解析
while ((read = isr2.read()) != -1) {
System.out.print((char)read);// 大家好
}
isr2.close();
}
}
```
### 2.4 OutputStreamWriter类
转换流`java.io.OutputStreamWriter` 是Writer的子类是从字符流到字节流的桥梁。使用指定的字符集讲字符编码为字节。它的字符集可以由名称指定也可以接受平台的默认字符集。
#### 构造方法
- `OutputStreamWriter(OutputStream in)`: 创建一个使用默认字符集的字符流。
- `OutputStreamWriter(OutputStream in, String charsetName)`: 创建一个指定字符集的字符流。
构造举例,代码如下:
```java
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");
```
#### 指定编码写出
```java
public class OutputDemo {
public static void main(String[] args) throws IOException {
// 定义文件路径
String FileName = "E:\\out.txt";
// 创建流对象,默认UTF8编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName));
// 写出数据
osw.write("你好"); // 保存为6个字节
osw.close();
// 定义文件路径
String FileName2 = "E:\\out2.txt";
// 创建流对象,指定GBK编码
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),"GBK");
// 写出数据
osw2.write("你好");// 保存为4个字节
osw2.close();
}
}
```
#### 转换流理解图解
**转换流是字节与字符间的桥梁!**![](img/2_zhuanhuan.jpg)
### 2.5 练习:转换文件编码
将GBK编码的文本文件转换为UTF-8编码的文本文件。
#### 案例分析
1. 指定GBK编码的转换流读取文本文件。
2. 使用UTF-8编码的转换流写出文本文件。
#### 案例实现
```java
public class TransDemo {
public static void main(String[] args) {
// 1.定义文件路径
String srcFile = "file_gbk.txt";
String destFile = "file_utf8.txt";
// 2.创建流对象
// 2.1 转换输入流,指定GBK编码
InputStreamReader isr = new InputStreamReader(new FileInputStream(srcFile) , "GBK");
// 2.2 转换输出流,默认utf8编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(destFile));
// 3.读写数据
// 3.1 定义数组
char[] cbuf = new char[1024];
// 3.2 定义长度
int len;
// 3.3 循环读取
while ((len = isr.read(cbuf))!=-1) {
// 循环写出
osw.write(cbuf,0,len);
}
// 4.释放资源
osw.close();
isr.close();
}
}
```
## 第三章 序列化
### 3.1 概述
Java 提供了一种对象**序列化**的机制。用一个字节序列可以表示一个对象,该字节序列包含该`对象的数据`、`对象的类型`和`对象中存储的数据`等信息。字节序列写出到文件之后,相当于文件中**持久保存**了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行**反序列化**。`对象的数据`、`对象的类型`和`对象中存储的数据`信息,都可以用来在内存中创建对象。看图理解序列化: ![](img/3_xuliehua.jpg)
### 3.2 ObjectOutputStream类
`java.io.ObjectOutputStream ` 类将Java对象的原始数据类型写出到文件,实现对象的持久存储。
#### 构造方法
* `public ObjectOutputStream(OutputStream out) ` 创建一个指定OutputStream的ObjectOutputStream。
构造举例,代码如下:
```java
FileOutputStream fileOut = new FileOutputStream("employee.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
```
#### 序列化操作
1. 一个对象要想序列化,必须满足两个条件:
* 该类必须实现`java.io.Serializable ` 接口,`Serializable` 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出`NotSerializableException` 。
* 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用`transient` 关键字修饰。
```java
public class Employee implements java.io.Serializable {
public String name;
public String address;
public transient int age; // transient瞬态修饰成员,不会被序列化
public void addressCheck() {
System.out.println("Address check : " + name + " -- " + address);
}
}
```
2.写出对象方法
* `public final void writeObject (Object obj)` : 将指定的对象写出。
```java
public class SerializeDemo{
public static void main(String [] args) {
Employee e = new Employee();
e.name = "zhangsan";
e.address = "beiqinglu";
e.age = 20;
try {
// 创建序列化流对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
// 写出对象
out.writeObject(e);
// 释放资源
out.close();
fileOut.close();
System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。
} catch(IOException i) {
i.printStackTrace();
}
}
}
输出结果:
Serialized data is saved
```
### 3.3 ObjectInputStream类
ObjectInputStream反序列化流将之前使用ObjectOutputStream序列化的原始数据恢复为对象。
#### 构造方法
* `public ObjectInputStream(InputStream in) ` 创建一个指定InputStream的ObjectInputStream。
#### 反序列化操作1
如果能找到一个对象的class文件我们可以进行反序列化操作调用`ObjectInputStream`读取对象的方法:
- `public final Object readObject ()` : 读取一个对象。
```java
public class DeserializeDemo {
public static void main(String [] args) {
Employee e = null;
try {
// 创建反序列化流
FileInputStream fileIn = new FileInputStream("employee.txt");
ObjectInputStream in = new ObjectInputStream(fileIn);
// 读取一个对象
e = (Employee) in.readObject();
// 释放资源
in.close();
fileIn.close();
}catch(IOException i) {
// 捕获其他异常
i.printStackTrace();
return;
}catch(ClassNotFoundException c) {
// 捕获类找不到异常
System.out.println("Employee class not found");
c.printStackTrace();
return;
}
// 无异常,直接打印输出
System.out.println("Name: " + e.name); // zhangsan
System.out.println("Address: " + e.address); // beiqinglu
System.out.println("age: " + e.age); // 0
}
}
```
**对于JVM可以反序列化对象它必须是能够找到class文件的类。如果找不到该类的class文件则抛出一个 `ClassNotFoundException` 异常。**
#### **反序列化操作2**
**另外当JVM反序列化对象时能找到class文件但是class文件在序列化对象之后发生了修改那么反序列化操作也会失败抛出一个`InvalidClassException`异常。**发生这个异常的原因如下:
* 该类的序列版本号与从流中读取的类描述符的版本号不匹配
* 该类包含未知数据类型
* 该类没有可访问的无参数构造方法
`Serializable` 接口给需要序列化的类,提供了一个序列版本号。`serialVersionUID` 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。
```java
public class Employee implements java.io.Serializable {
// 加入序列版本号
private static final long serialVersionUID = 1L;
public String name;
public String address;
// 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值.
public int eid;
public void addressCheck() {
System.out.println("Address check : " + name + " -- " + address);
}
}
```
### 3.4 练习:序列化集合
1. 将存有多个自定义对象的集合序列化操作,保存到`list.txt`文件中。
2. 反序列化`list.txt` ,并遍历集合,打印对象信息。
#### 案例分析
1. 把若干学习对象 ,保存到集合中。
2. 把集合序列化。
3. 反序列化读取时,只需要读取一次,转换为集合类型。
4. 遍历集合,可以打印所有的学生信息
#### 案例实现
```java
public class SerTest {
public static void main(String[] args) throws Exception {
// 创建 学生对象
Student student = new Student("老王", "laow");
Student student2 = new Student("老张", "laoz");
Student student3 = new Student("老李", "laol");
ArrayList<Student> arrayList = new ArrayList<>();
arrayList.add(student);
arrayList.add(student2);
arrayList.add(student3);
// 序列化操作
// serializ(arrayList);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt"));
// 读取对象,强转为ArrayList类型
ArrayList<Student> list = (ArrayList<Student>)ois.readObject();
for (int i = 0; i < list.size(); i++ ){
Student s = list.get(i);
System.out.println(s.getName()+"--"+ s.getPwd());
}
}
private static void serializ(ArrayList<Student> arrayList) throws Exception {
// 创建 序列化流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
// 写出对象
oos.writeObject(arrayList);
// 释放资源
oos.close();
}
}
```
## 第四章 打印流
### 4.1 概述
平时我们在控制台打印输出,是调用`print`方法和`println`方法完成的,这两个方法都来自于`java.io.PrintStream`类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。
### 4.2 PrintStream类
#### 构造方法
* `public PrintStream(String fileName) ` 使用指定的文件名创建一个新的打印流。
构造举例,代码如下:
```java
PrintStream ps = new PrintStream("ps.txt")
```
#### 改变打印流向
`System.out`就是`PrintStream`类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个"小把戏",改变它的流向。
```java
public class PrintDemo {
public static void main(String[] args) throws IOException {
// 调用系统的打印流,控制台直接输出97
System.out.println(97);
// 创建打印流,指定文件的名称
PrintStream ps = new PrintStream("ps.txt");
// 设置系统的打印流流向,输出到ps.txt
System.setOut(ps);
// 调用系统的打印流,ps.txt中输出97
System.out.println(97);
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,556 @@
# day11【网络编程】
### 主要内容
* 软件架构CSBS
* 网络通信三要素
* TCP通信
* Socket套接字
* ServerSocket
### 教学目标
- [ ] 能够辨别UDP和TCP协议特点
- [ ] 能够说出TCP协议下两个常用类名称
- [ ] 能够编写TCP协议下字符串数据传输程序
- [ ] 能够理解TCP协议下文件上传案例
- [ ] 能够理解TCP协议下BS案例
## 第一章 网络编程入门
### 1.1软件结构
- **C/S结构** 全称为Client/Server结构是指客户端和服务器结构。常见程序有、迅雷等软件。
![](img/1_cs.jpg)
**B/S结构** 全称为Browser/Server结构是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。
![](img/2_bs.jpg)
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。**网络编程**,就是在一定的协议下,实现两台计算机的通信的程序。
### 1.2 网络通信协议
* **网络通信协议:**通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。
* **TCP/IP协议** 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol)是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议并采用了4层的分层模型每一层都呼叫它的下一层所提供的协议来完成自己的需求。
![](img/3_tcp_ip.jpg)
### 1.3 协议分类
通信的协议还是比较复杂的,`java.net` 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。
`java.net` 包中提供了两种常见的网络协议的支持:
- **TCP**:传输控制协议 (Transmission Control Protocol)。TCP协议是**面向连接**的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
- 三次握手TCP协议中在发送数据的准备阶段客户端与服务器之间的三次交互以保证连接的可靠。
- 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
- 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
- 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。
![](img/4_tcp.jpg)
完成三次握手连接建立后客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性TCP协议可以保证传输数据的安全所以应用十分广泛例如下载文件、浏览网页等。
- **UDP**:用户数据报协议(User Datagram Protocol)。UDP协议是一个**面向无连接**的协议。传输数据时不需要建立连接不管对方端服务是否启动直接将数据、数据源和目的地都封装在数据包中直接发送。每个数据包的大小限制在64k以内。它是不可靠协议因为无连接所以传输速度快但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。
### 1.4 网络编程三要素
#### 协议
* **协议:**计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。
#### IP地址
* **IP地址指互联网协议地址Internet Protocol Address**俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话那么“IP地址”就相当于“电话号码”。
**IP地址分类**
* IPv4是一个32位的二进制数通常被分为4个字节表示成`a.b.c.d` 的形式,例如`192.168.65.100` 。其中a、b、c、d都是0~255之间的十进制整数那么最多可以表示42亿个。
* IPv6由于互联网的蓬勃发展IP地址的需求量愈来愈大但是网络地址资源有限使得IP的分配越发紧张。有资料显示全球IPv4地址在2011年2月分配完毕。
为了扩大地址空间拟通过IPv6重新定义地址空间采用128位地址长度每16个字节一组分成8组十六进制数表示成`ABCD:EF01:2345:6789:ABCD:EF01:2345:6789`,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
**常用命令**
* 查看本机IP地址在控制台输入
```java
ipconfig
```
* 检查网络是否连通,在控制台输入:
```java
ping 空格 IP地址
ping 220.181.57.216
```
**特殊的IP地址**
* 本机IP地址`127.0.0.1``localhost`
#### 端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说**IP地址**可以唯一标识网络中的设备,那么**端口号**就可以唯一标识设备中的进程(应用程序)了。
* **端口号用两个字节表示的整数它的取值范围是0~65535**。其中0~1023之间的端口号用于一些知名的网络服务和应用普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用会导致当前程序启动失败。
利用`协议`+`IP地址`+`端口号` 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
## 第二章 TCP通信程序
### 2.1 概述
TCP通信能实现两台计算机之间的数据交互通信的两端要严格区分为客户端Client与服务端Server
**两端通信时步骤:**
1. 服务端程序,需要事先启动,等待客户端的连接。
2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
**在Java中提供了两个类用于实现TCP通信程序**
1. 客户端:`java.net.Socket` 类表示。创建`Socket`对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
2. 服务端:`java.net.ServerSocket` 类表示。创建`ServerSocket`对象,相当于开启一个服务,并等待客户端的连接。
### 2.2 Socket类
`Socket` 类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。
#### 构造方法
* `public Socket(String host, int port)` :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址。
> 小贴士:回送地址(127.x.x.x) 是本机回送地址Loopback Address主要用于网络软件测试以及本地机进程间通信无论什么程序一旦使用回送地址发送数据立即返回不进行任何网络传输。
构造举例,代码如下:
```java
Socket client = new Socket("127.0.0.1", 6666);
```
#### 成员方法
* `public InputStream getInputStream()` 返回此套接字的输入流。
* 如果此Scoket具有相关联的通道则生成的InputStream 的所有操作也关联该通道。
* 关闭生成的InputStream也将关闭相关的Socket。
* `public OutputStream getOutputStream()` 返回此套接字的输出流。
* 如果此Scoket具有相关联的通道则生成的OutputStream 的所有操作也关联该通道。
* 关闭生成的OutputStream也将关闭相关的Socket。
* `public void close()` :关闭此套接字。
* 一旦一个socket被关闭它不可再使用。
* 关闭此socket也将关闭相关的InputStream和OutputStream 。
* `public void shutdownOutput()` 禁用此套接字的输出流。
* 任何先前写出的数据将被发送,随后终止输出流。
### 2.3 ServerSocket类
`ServerSocket`类:这个类实现了服务器套接字,该对象等待通过网络的请求。
#### 构造方法
* `public ServerSocket(int port)` 使用该构造方法在创建ServerSocket对象时就可以将其绑定到一个指定的端口号上参数port就是端口号。
构造举例,代码如下:
```java
ServerSocket server = new ServerSocket(6666);
```
#### 成员方法
* `public Socket accept()` 侦听并接受连接返回一个新的Socket对象用于和客户端实现通信。该方法会一直阻塞直到建立连接。
### 2.4 简单的TCP网络程序
#### TCP通信分析图解
1. 【服务端】启动,创建ServerSocket对象等待连接。
2. 【客户端】启动,创建Socket对象请求连接。
3. 【服务端】接收连接,调用accept方法并返回一个Socket对象。
4. 【客户端】Socket对象获取OutputStream向服务端写出数据。
5. 【服务端】Scoket对象获取InputStream读取客户端发送的数据。
> 到此,客户端向服务端发送数据成功。
![](img/5_简单通信.jpg)
> 自此,服务端向客户端回写数据。
6. 【服务端】Socket对象获取OutputStream向客户端回写数据。
7. 【客户端】Scoket对象获取InputStream解析回写数据。
8. 【客户端】释放资源,断开连接。
#### 客户端向服务器发送数据
**服务端实现:**
```java
public class ServerTCP {
public static void main(String[] args) throws IOException {
System.out.println("服务端启动 , 等待连接 .... ");
// 1.创建 ServerSocket对象绑定端口开始等待连接
ServerSocket ss = new ServerSocket(6666);
// 2.接收连接 accept 方法, 返回 socket 对象.
Socket server = ss.accept();
// 3.通过socket 获取输入流
InputStream is = server.getInputStream();
// 4.一次性读取数据
// 4.1 创建字节数组
byte[] b = new byte[1024];
// 4.2 据读取到字节数组中.
int len = is.read(b)
// 4.3 解析数组,打印字符串信息
String msg = new String(b, 0, len);
System.out.println(msg);
//5.关闭资源.
is.close();
server.close();
}
}
```
**客户端实现:**
```java
public class ClientTCP {
public static void main(String[] args) throws Exception {
System.out.println("客户端 发送数据");
// 1.创建 Socket ( ip , port ) , 确定连接到哪里.
Socket client = new Socket("localhost", 6666);
// 2.获取流对象 . 输出流
OutputStream os = client.getOutputStream();
// 3.写出数据.
os.write("你好么? tcp ,我来了".getBytes());
// 4. 关闭资源 .
os.close();
client.close();
}
}
```
#### 服务器向客户端回写数据
**服务端实现:**
```java
public class ServerTCP {
public static void main(String[] args) throws IOException {
System.out.println("服务端启动 , 等待连接 .... ");
// 1.创建 ServerSocket对象绑定端口开始等待连接
ServerSocket ss = new ServerSocket(6666);
// 2.接收连接 accept 方法, 返回 socket 对象.
Socket server = ss.accept();
// 3.通过socket 获取输入流
InputStream is = server.getInputStream();
// 4.一次性读取数据
// 4.1 创建字节数组
byte[] b = new byte[1024];
// 4.2 据读取到字节数组中.
int len = is.read(b)
// 4.3 解析数组,打印字符串信息
String msg = new String(b, 0, len);
System.out.println(msg);
// =================回写数据=======================
// 5. 通过 socket 获取输出流
OutputStream out = server.getOutputStream();
// 6. 回写数据
out.write("我很好,谢谢你".getBytes());
// 7.关闭资源.
out.close();
is.close();
server.close();
}
}
```
**客户端实现:**
```java
public class ClientTCP {
public static void main(String[] args) throws Exception {
System.out.println("客户端 发送数据");
// 1.创建 Socket ( ip , port ) , 确定连接到哪里.
Socket client = new Socket("localhost", 6666);
// 2.通过Scoket,获取输出流对象
OutputStream os = client.getOutputStream();
// 3.写出数据.
os.write("你好么? tcp ,我来了".getBytes());
// ==============解析回写=========================
// 4. 通过Scoket,获取 输入流对象
InputStream in = client.getInputStream();
// 5. 读取数据数据
byte[] b = new byte[100];
int len = in.read(b);
System.out.println(new String(b, 0, len));
// 6. 关闭资源 .
in.close();
os.close();
client.close();
}
}
```
## 第三章 综合案例
### 3.1 文件上传案例
#### 文件上传分析图解
1. 【客户端】输入流,从硬盘读取文件数据到程序中。
2. 【客户端】输出流,写出文件数据到服务端。
3. 【服务端】输入流,读取文件数据到服务端程序。
4. 【服务端】输出流,写出文件数据到服务器硬盘中。
![](img/6_upload.jpg)
##### 基本实现
**服务端实现:**
```java
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("服务器 启动..... ");
// 1. 创建服务端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 建立连接
Socket accept = serverSocket.accept();
// 3. 创建流对象
// 3.1 获取输入流,读取文件数据
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
// 3.2 创建输出流,保存到本地 .
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"));
// 4. 读写数据
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
//5. 关闭 资源
bos.close();
bis.close();
accept.close();
System.out.println("文件上传已保存");
}
}
```
**客户端实现:**
```java
public class FileUPload_Client {
public static void main(String[] args) throws IOException {
// 1.创建流对象
// 1.1 创建输入流,读取本地文件
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
// 1.2 创建输出流,写到服务端
Socket socket = new Socket("localhost", 6666);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//2.写出数据.
byte[] b = new byte[1024 * 8 ];
int len ;
while (( len = bis.read(b))!=-1) {
bos.write(b, 0, len);
bos.flush();
}
System.out.println("文件发送完毕");
// 3.释放资源
bos.close();
socket.close();
bis.close();
System.out.println("文件上传完毕 ");
}
}
```
#### 文件上传优化分析
1. **文件名称写死的问题**
服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一,代码如下:
```java
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+".jpg") // 文件名称
BufferedOutputStream bos = new BufferedOutputStream(fis);
```
2. **循环接收的问题**
服务端,指保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件,代码如下:
```java
// 每次接收新的连接,创建一个Socket
whiletrue{
Socket accept = serverSocket.accept();
......
}
```
3. **效率问题**
服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化,代码如下:
```java
whiletrue{
Socket accept = serverSocket.accept();
// accept 交给子线程处理.
new Thread(() -> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}
```
##### 优化实现
```java
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("服务器 启动..... ");
// 1. 创建服务端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 循环接收,建立连接
while (true) {
Socket accept = serverSocket.accept();
/*
3. socket对象交给子线程处理,进行读写操作
Runnable接口中,只有一个run方法,使用lambda表达式简化格式
*/
new Thread(() -> {
try (
//3.1 获取输入流对象
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//3.2 创建输出流对象, 保存到本地 .
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);) {
// 3.3 读写数据
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
//4. 关闭 资源
bos.close();
bis.close();
accept.close();
System.out.println("文件上传已保存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
```
#### 信息回写分析图解
前四步与基本文件上传一致.
5. 【服务端】获取输出流,回写数据。
6. 【客户端】获取输入流,解析回写数据。
![](img/6_upload2.jpg)
##### 回写实现
```java
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("服务器 启动..... ");
// 1. 创建服务端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 循环接收,建立连接
while (true) {
Socket accept = serverSocket.accept();
/*
3. socket对象交给子线程处理,进行读写操作
Runnable接口中,只有一个run方法,使用lambda表达式简化格式
*/
new Thread(() -> {
try (
//3.1 获取输入流对象
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//3.2 创建输出流对象, 保存到本地 .
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);
) {
// 3.3 读写数据
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
// 4.=======信息回写===========================
System.out.println("back ........");
OutputStream out = accept.getOutputStream();
out.write("上传成功".getBytes());
out.close();
//================================
//5. 关闭 资源
bos.close();
bis.close();
accept.close();
System.out.println("文件上传已保存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
```
**客户端实现:**
```java
public class FileUpload_Client {
public static void main(String[] args) throws IOException {
// 1.创建流对象
// 1.1 创建输入流,读取本地文件
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
// 1.2 创建输出流,写到服务端
Socket socket = new Socket("localhost", 6666);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//2.写出数据.
byte[] b = new byte[1024 * 8 ];
int len ;
while (( len = bis.read(b))!=-1) {
bos.write(b, 0, len);
}
// 关闭输出流,通知服务端,写出数据完毕
socket.shutdownOutput();
System.out.println("文件发送完毕");
// 3. =====解析回写============
InputStream in = socket.getInputStream();
byte[] back = new byte[20];
in.read(back);
System.out.println(new String(back));
in.close();
// ============================
// 4.释放资源
socket.close();
bis.close();
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Some files were not shown because too many files have changed in this diff Show More