Java基础(下)
Stream流
Stream 流和工厂流水线差不多,都是通过对数据进行过滤而得到最终结果。
Stream 流的作用:结合了 Lambda 表达式,简化集合、数组的操作。
Stream 流的使用步骤:
- 先得到一条 Stream 流,并把数据放上去。
- 利用 Stream 流中的 API 进行各种操作,先使用中间方法对数据进行操作,最后使用终结方法对数据进行操作。
Stream 流的获取
获取方式 | 方法名 | 说明 |
---|---|---|
单列集合 | default Stream <E> stream() |
Collection 中的默认方法 |
双列集合 | 无 |
无法直接使用 Stream 流,一般会通过 keySet 或者 entrySet 转换成单列集合,然后再启用 Stream 流 |
数组 | public static <T> Stream <T> stream(T[] array) |
Arrays 工具类中的静态方法 |
一堆零散数据 | public static <T> Stream <T> of(T...values) |
Stream 接口中的静态方法 |
import java.util.*;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
//单列集合获取Stream流
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "b", "b", "b", "c", "d", "e");
//获取到一条流水线,并将数据放在流水线上
Stream<String> stream1 = list.stream();
//使用终结方法打印数据
stream1.forEach((String s)->{
//s:依次表示stream中的每一个数据
System.out.println(s);
}
);
System.out.println("------");
//双列集合获取Stream流
HashMap<String, Integer> hm = new HashMap<>();
hm.put("a", 1);
hm.put("b", 2);
hm.put("c", 3);
hm.put("d", 4);
//获取Stream流,需要先转化成单列集合
Set<String> keySet = hm.keySet();
keySet.stream().forEach(s-> System.out.println(s));
System.out.println("-----");
hm.entrySet().stream().forEach(s-> System.out.println(s));
System.out.println("-----");
//数组获取Stream流
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Arrays.stream(arr).forEach(integer-> System.out.println(integer));
System.out.println("-----");
//零散数据获取Stream流
Stream.of("abc", 2, "3", 4.1, 5).forEach(data-> System.out.println(data));
System.out.println("-----");
Stream.of(arr).forEach(integer -> System.out.println(integer)); //[I@27bc2616
//Stream接口中的of方法如果传递数组,需要传递引用数据类型的数组,而不是基本数据类型的数组
//否则会被当作一个整体,打印出地址值
}
}
Stream 流的中间方法
名称 | 说明 |
---|---|
Stream<T> filter(Predicate<? super T> predicate) |
过滤 |
Stream<T> limit(long maxSize) |
获取前几个元素 |
Stream<T> skip(long n) |
跳过前几个元素 |
Stream<T> distinct() |
元素去重(依赖 hashCode 和 equals,因为底层是利用 HashSet 进行去重的) |
static<T> Stream<T> concat(Stream a, Stream b) |
合并 a 流和 b 流 |
Stream<R> map(Function<T, R> mapper) |
转换流中的数据类型 |
Stream<T> sorted() |
对流中的数据进行排序 |
注意:
- 中间方法会返回新的 Stream 流,原来的 Stream 流只能使用依次,建议使用链式编程。
- 修改 Stream 流中的数据,不会影响原来集合或者数组中的数据。
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "aaa", "bbb", "ccc", "bcd", "cdb", "bd");
//filter过滤,把b开头的留下
list.stream().filter((String s)->{
//如果返回值为true,则表示当前数据留下,反之则舍弃
return s.startsWith("b");
}
).forEach(s -> System.out.println(s));
System.out.println("-----");
//可以通过换行增强代码的阅读性
list.stream()
.filter(str->str.startsWith("b"))
.filter(str->str.length() == 3)
.forEach(str -> System.out.println(str));
System.out.println("-----");
//limit获取前几个元素
list.stream().limit(3).forEach(str -> System.out.println(str));
System.out.println("-----");
//skip跳元素
list.stream().skip(3).forEach(str -> System.out.println(str));
System.out.println("-----");
//获取中间元素
list.stream().skip(3).limit(2).forEach(str -> System.out.println(str));
System.out.println("------");
ArrayList<Integer> list1 = new ArrayList<>();
Collections.addAll(list1, 1, 1, 1, 2, 3, 3, 4, 5);
list1.stream().distinct().forEach(integer -> System.out.println(integer));
//如果集合中装的是其他类,则需要手写hashCode和equals方法
System.out.println("-----");
//利用concat合并两个流
Stream.concat(list1.stream(), list.stream()).forEach(data -> System.out.println(data));
System.out.println("-----");
//转换流中的数据类型
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2, "张三-18", "王五-19", "赵六-20");
//只获取年龄进行打印
list2.stream().map(new Function<String, Integer>() {
//Function中第一个类型表示原本的类型,第二个类型表示转换之后的类型
@Override
public Integer apply(String s) {
//s表示里面的每一个数据
//返回值表示转换之后的数据
String[] arr = s.split("-"); //利用split进行切割
String ageStr = arr[1];
Integer age = Integer.parseInt(ageStr);
return age;
}
}).forEach(integer -> System.out.println(integer));
//转化之后,stream中的数据就变成了Integer类型了
System.out.println("-----");
//Lambda表达式写法
list2.stream()
.map(s -> Integer.parseInt(s.split("-")[1]))
.forEach(integer -> System.out.println(integer));
}
}
Stream 流的终结方法
名称 | 说明 |
---|---|
void forEach(Consumer action) |
遍历 |
long count() |
统计 |
toArray() |
收集流中的数据,放到数组中 |
collect(Collector collector) |
收集流中的数据,放到集合中(如果要收录到 Map 集合中,键是不能够重复的) |
import java.util.*;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "test0", "test1", "test2", "test3", "test4");
//forEach遍历
list.stream().forEach(str -> System.out.println(str));
//count统计stream长度
long count = list.stream().count();
System.out.println(count);
//toArray方法,将流中的数据收录到数组当中
Object[] objectArray = list.stream().toArray();
System.out.println(Arrays.toString(objectArray));
String[] stringArr = list.stream().toArray(new IntFunction<String[]>() {
//? extends Object[] 表示具体类型的数组
@Override
public String[] apply(int value) { //value表示数据的个数
return new String[value]; //返回值是具体类型的数组
}
});
System.out.println(Arrays.toString(stringArr));
//Lambda表达式改写
String[] stringArr1 = list.stream().toArray(value -> new String[value]);
System.out.println(Arrays.toString(stringArr1));
}
}
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "test0-男-18", "test1-女-19", "test2-女-19", "test3-男-20", "test4-男-22");
//收集到List集合
//将所有的男性收集起来
List<String> collect = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toList());
//收集到Set集合
Set<String> collect1 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toSet());
//收集到Map集合
//键:姓名 值:年龄
Map<String, Integer> collect2 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(new Function<String, String>() {
@Override
public String apply(String s) { //键的规则
return s.split("-")[0];
}
}, new Function<String, Integer>() {
@Override
public Integer apply(String s) { //值的规则
return Integer.parseInt(s.split("-")[2]);
}
}));
System.out.println(collect2); //收录到Map中,需要保证键不重复
//利用Lambda表达式简化
Map<String, Integer> collect3 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(
str -> str.split("-")[0],
str -> Integer.parseInt(str.split("-")[2])));
System.out.println(collect3);
}
}
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<>();
Collections.addAll(list, new Student("zhangsan", 18),
new Student("lisi", 18),
new Student("wangwu", 19),
new Student("zhaoliu", 20),
new Student("张三", 19),
new Student("李四", 20));
//现利用Collectors.groupingBy()方法进行对list进行分组
Map<Integer, List<Student>> collect = list.stream().collect(Collectors.groupingBy(Student::getAge));
for (Integer integer : collect.keySet()) {
System.out.println(collect.get(integer));
}
//现利用Collectors.counting()方法对学生进行分组后的统计
Map<Integer, Long> collect1 = list.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.counting()));
for (Integer integer : collect1.keySet()) {
System.out.println(collect1.get(integer));
}
//计算年龄平均数
Double aver = list.stream().collect(Collectors.averagingInt(Student::getAge));
System.out.println(aver);
//计算年龄平均数的另一种写法
double aver1 = list.stream().mapToInt(Student::getAge).average().getAsDouble();
System.out.println(aver1);
}
}
基本数据类型的 Stream
Stream 是对象类型对应的流。而在 Java 中,还提供了多种对应基本数据类型的流,分别是:IntStream
、LongStream
、DoubleStream
。
普通数组通过Arrays.stream()
方法获取得到的流便是基本数据流,对于对象类型的流,可以通过mapToInt
、mapToLong
、mapToDouble
的方法转换为基本数据流。
Java 为基本数据流提供了一些特有的接口方法,使得我们可以更加便捷的操作数据。
import java.util.Arrays;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
//普通数组转换而成的便是一个基本数据流
IntStream intStream = Arrays.stream(arr);
//sum 求和
System.out.println(intStream.sum()); //15
//max 求最大值
System.out.println(Arrays.stream(arr).max().getAsInt());//5
//min 求最小值
System.out.println(Arrays.stream(arr).min().getAsInt());//1
}
}
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
//现利用基本数据类型对map的value进行求和
HashMap<Integer, String> map = new HashMap<>();
initMap(map);
//思路
//先获取map集合的所有值,转为int后加和
int sum = map.values().stream().mapToInt(Integer::parseInt).sum();
System.out.println(sum); //59
}
public static void initMap(HashMap<Integer, String> map) {
map.put(1, "12");
map.put(2, "12");
map.put(3, "10");
map.put(4, "11");
map.put(5, "14");
}
}
方法引用
把已经有的方法拿过来用,当作函数式接口中抽象方法的方法体。
注意:
- 引用处必须是函数式接口。
- 被引用的方法必须已经存在。
- 被引用的方法的形参和返回值需要跟抽象方法保持一致。
- 被引用的方法的功能需要满足当前需求。
引用静态方法
格式:类名::静态方法名
import java.util.*;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "1", "2", "3", "4", "5");
//现将String转化为整型
List<Integer> list1 = list.stream().map(Integer::parseInt).toList();
}
}
引用成员方法
格式:对象::成员方法
。如果是本类,则使用this::成员方法
。调用父类则用super::成员方法
。
静态方法中是不能使用 this 和 super 的,故如果需要使用 this 和 super 来进行方法引用,需要保证在引用时引用处不是静态的。
import java.util.*;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//需要过滤出以张开头的,名字长度为3的人物
list.stream()
.filter(str -> str.startsWith("张"))
.filter(str -> str.length() == 3).forEach(str -> System.out.println(str));
//利用方法引用进行更改
StringOperation so = new StringOperation();
list.stream().filter(so::stringJudge).forEach(System.out::println);
}
}
引用构造方法
格式:类名::new
public class Student {
private String name;
private int age;
//...
//需要有参数和apply方法一致的构造方法
public Student(String s) {
String name = s.split(",")[0];
int age = Integer.parseInt(s.split(",")[1]);
this.name = name;
this.age = age;
}
//...
}
import java.util.*;
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌,15", "周芷若,14", "赵敏,13", "张强,12", "张三丰,11");
//封装成Student对象并收集到List集合中
/*List<Student> list1 = list.stream().map(new Function<String, Student>() {
@Override
public Student apply(String s) {
String name = s.split(",")[0];
int age = Integer.parseInt(s.split(",")[1]);
return new Student(name, age);
}
}).toList();*/
List<Student> list1 = list.stream().map(Student::new).toList();
System.out.println(list1);
}
}
使用类名引用成员方法
格式:类名::成员方法
规则如下:
- 需要有函数式接口。
- 被引用的方法必须已经存在。
- 被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值需要保持一致。
- 被引用的方法的功能需要满足当前的需求。
该方法具有一定的局限性:即不能引用所有类中的成员方法。并且跟抽象方法的第一个参数有关,这个参数是什么类型的,那么就只能引用这个类中的方法。
import java.util.*;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "aaa", "bbb", "ccc", "ddd");
//现将数据变为大写后输出
list.stream().map(String::toUpperCase).forEach(System.out::println);
//拿着stream流当中的每一个数据去调用toUpperCase的方法,方法的返回值就是最后的结果
//因为是拿着数据去调用的,所以要求填入的方法引用的类名必须和数据类型保持一致
}
}
引用数组的构造方法
格式:数据类型[]::new
引用数组的构造方法是为了创建一个数组。
import java.util.*;
import java.util.function.IntFunction;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);
//收集到数组中
/*list.stream().toArray(new IntFunction<Integer[]>() {
@Override
public Integer[] apply(int value) {
return new Integer[value];
}
});*/
//数组的类型需要和流中的数据类型保持一致
Integer[] arr = list.stream().toArray(Integer[]::new);
System.out.println(Arrays.toString(arr));
}
}
异常
异常就是代表程序出现的问题。其父类是 Throwable。其下两个子类,Error 和 Exception。Error 代表的是系统级别的错误(严重问题),是SUN公司自己用的。而 Exception 代表程序可能出现的问题。Exception 分为 RuntimeException 和其他异常。其中,RuntimeException 是运行时异常,编译阶段不会被检测出来,编译阶段 Java 不会执行代码,只会检查语法是否错误,或者做一些性能优化。编译时异常更多是提醒程序员检查本地信息,运行时异常是代码逻辑出错而导致程序出现的问题。
idea 当中使用 Ctrl + Alt + T 进行 try catch 包裹。
public class Main {
public static void main(String[] args) {
//编译时异常,多为语法错误,这里不多做演示
//运行时异常
int[] arr = {1, 2, 3, 4};
System.out.println(arr[10]);
//ArrayIndexOutOfBoundsException,数组越界
}
}
异常的作用
- 是用来查询bug的关键参考信息。
- 异常可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况(看异常的发生位置可以从下往上读)。
class Student {
int age;
String name;
void setAge(int age) {
if(age < 18 || age > 40) {
throw new RuntimeException(); //抛出异常
}
this.age = age;
}
}
public class Main {
public static void main(String[] args) {
Student s1 = new Student();
s1.setAge(19);
s1.setAge(17); //RuntimeException
}
}
异常的处理方式
异常的常见处理方式有:
- JVM默认处理。
- 捕获异常。
- 抛出异常。
其中,抛出主要是告诉调用者出错了。而捕获主要是为了不让程序停止。
JVM默认处理
把异常的名称,异常原因以及异常出现的位置等信息用红色字体打印在控制台上。并且此时的程序停止,代码不再执行。如果等号左右两侧都有异常,则默认先捕捉右侧的异常。
捕获异常
目的是让异常出现的时候,让程序继续执行。基本语法为:
try {
可能出现异常的代码;
} catch (异常类名 变量名){
异常的处理代码;
}
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
try {
//可能出现异常的代码
System.out.println(arr[10]);
//这里会创建ArrayIndexOutOfBoundsException的对象
//与catch中的e对比,看看类型是否匹配,匹配了,就让程序继续进行
} catch (ArrayIndexOutOfBoundsException e) {
//出现异常后该如何处理
System.out.println("索引越界了");
}
System.out.println("看看我执行了吗");
//最终执行的结果为:
//索引越界了
//看看我执行了吗
}
}
关于捕获异常的四个注意点:
- 如果 try 中没有遇到问题,会执行 try 当中的所有代码,不会执行 catch 的代码。也就是说,catch 当中的代码只有出现了了异常才会执行。
- 如果 try 中遇到多个问题,如果第一个问题能够被顺利捕获,则接下来 try 中代码便不会执行。最佳解决方案是写多个 catch 捕获多个问题(一行一个 catch或者一行中利用
|
连接),并且,如果异常之间有父子类关系的话,父类一定要写在下面。 - 如果 try 中遇到的问题没有被捕获,则使用 JVM 默认处理异常的方式进行处理。
- 如果 try 中遇到了问题,try 下面的其他代码便不会执行了,直接跳到 catch,但如果没有 catch 与之匹配,则按照 JVM 默认处理异常的方式进行处理。
抛出异常
- throws:写在方法定义处,表示声明一个异常。告诉调用者,使用本方法可能会有哪些异常。如果是编译时异常,必须要写,如果是运行时异常,可以不写。
- throw:写在方法内,结束方法。手动抛出异常对象,交给调用者。方法中下面的代码不在执行了。
父类没有抛异常,子类重写父类的方法时也不能抛异常。
public class Main {
public static void main(String[] args) {
int[] arr = null;
int sum = 0;
//处理时使用try catch捕获
try {
sum = getSum(arr);
} catch (NullPointerException e) {
System.out.println("空指针异常");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("索引异常");
}
System.out.println(sum);
}
//定义一个方法求总和
public static int getSum(int[] number)
throws NullPointerException, ArrayIndexOutOfBoundsException { //可以声明异常
if(number == null) {
throw new NullPointerException(); //也可以抛出异常
}
if(number.length == 0) {
throw new ArrayIndexOutOfBoundsException();
}
int result = 0;
for(int index : number) {
result += index;
}
return result;
}
}
异常中的常见方法
方法名称 | 说明 |
---|---|
public String getMessage() |
返回此 throwable 的详细消息字符串 |
public String toString() |
返回此可抛出的简短描述 |
public void printStackTrace() |
把异常的错误信息输出在控制台,但不停止虚拟机的运行 |
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
try {
System.out.println(arr[10]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage()); //Index 10 out of bounds for length 4
//以上语句会打印异常的消息
//java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 4
System.out.println(e.toString());
e.printStackTrace(); //打印操作,但实际不会停止虚拟机,因为第16行的测试代码会被成功打印
}
System.out.println("看看我执行了吗");
}
}
自定义异常
自定义异常的目的是为了让报错信息更加见名知意。步骤如下:
- 定义异常类(编译时异常继承 Exception,运行时异常继承 RuntimeException)。
- 写继承关系。
- 空参构造。
- 带参构造。
import java.util.Scanner;
class NameFormatException extends RuntimeException{ //自定义异常
//NameFormat:当前异常的名字,表示姓名格式化问题
//Exception:后缀
public NameFormatException() {
}
public NameFormatException(String message) {
super(message);
}
}
class AgeOutOfBoundException extends RuntimeException {
public AgeOutOfBoundException() {
}
public AgeOutOfBoundException(String message) {
super(message);
}
}
class GirlFriend {
int age;
String name;
void setAge(int age) {
if(age < 18 || age > 40) {
throw new AgeOutOfBoundException("年龄不符范围");
}
this.age = age;
}
void setName(String name) {
int len = name.length();
if(len < 3 || len > 10) {
throw new NameFormatException("name格式有误"); //名字格式不符合要求,抛出异常
}
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
//键盘录入练习
//需要考虑所有异常录入的情况
Scanner scanner = new Scanner(System.in);
GirlFriend girlFriend = new GirlFriend();
while (true) {
try {
System.out.println("录入姓名:");
String name = scanner.nextLine();
girlFriend.setName(name);
System.out.println("录入年龄:");
String ageStr = scanner.nextLine(); //录入所有可能出现的情况,使用nextLine
girlFriend.setAge(Integer.parseInt(ageStr));
break; //如果所有的数据输入正确,跳出循环
}
catch (NumberFormatException e ) {
System.out.println("年龄输入有误,请输入数字");
}
catch (AgeOutOfBoundException r) {
r.printStackTrace(); //抛异常,但是不停止虚拟机运行
}
catch (NameFormatException n) {
n.printStackTrace();
}
}
System.out.println(girlFriend.name + " " + girlFriend.age);
}
}
File
路径分为两种:相对路径和绝对路径。
相对路径是不带盘符的,相对于当前项目而言。而绝对路径是带盘符的,相对于整个计算机而言。
File 对象就表示一个路径,可以是文件的路径,也可以是文件夹的路径。这个路径可以是存在的,也可以是不存在的。
File 常见的构造方法如下:
方法名称 | 说明 |
---|---|
public File(String pathname) |
根据文件路径创建文件对象 |
public File(String parent, String child) |
根据父路径名字符串和子路径名字符串创建文件对象 |
public File(File parent, String child) |
根据父路径对应文件对象和子路径名字符串创建文件对象 |
import java.io.File;
public class Main {
public static void main(String[] args) {
//根据文件路径创建文件对象
String str = "D:\\HNU\\a.txt";
File f1 = new File(str);
System.out.println(f1);
//根据父路径和子路径创建对象
//父路径:D:\HNU
//子路径:a.txt
String parent = "D:\\HNU";
String child = "a.txt";
File f2 = new File(parent, child);
System.out.println(f2);
//根据父文件和子路径创建对象
File parent2 = new File("D:\\HNU");
String child2 = "a.txt";
File f3 = new File(parent2, child2);
System.out.println(f3);
}
}
File 的判断和获取
方法名称 | 说明 |
---|---|
public boolean isDirectory() |
判断此路径名表示的 File 是否为文件夹 |
public boolean isFile() |
判断此路径名表示的 File 是否为文件 |
public boolean exists() |
判断此路径名表示的 File 是否存在 |
public long length() |
返回文件的大小(字节数量) |
public String getAbsolutePath() |
返回文件的绝对路径 |
public String getPath() |
返回定义文件时使用的路径 |
public String getName() |
返回文件的名称,带后缀 |
public long lastModified() |
返回文件的最后修改时间(时间毫秒值) |
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
public static void main(String[] args) {
//判断是否为文件夹
File f1 = new File("D:\\HNU\\test.txt");
System.out.println(f1.isDirectory());
//判断是否为文件
System.out.println(f1.isFile());
//判断是否存在
System.out.println(f1.exists());
//返回字节大小,该方法无法获取文件夹的大小
//如果要获取文件夹的大小,则需要将该文件夹所有的文件大小累加到一起
long length = f1.length();
System.out.println(length);
//返回文件的绝对路径
String path = f1.getAbsolutePath();
System.out.println(path);
//返回定义文件时的路径
File f2 = new File("D:\\aaa\\a.txt");
System.out.println(f2.getPath()); //D:\aaa\a.txt
File f3 = new File("a.txt");
System.out.println(f3.getPath()); //a.txt
//获取名字
String name = f1.getName();
System.out.println(name);
//返回文件的最后修改时间
long lastTime = f1.lastModified();
System.out.println(lastTime); //1705385138211
//时间的转化
Date date = new Date(lastTime);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = sdf.format(date);
System.out.println(time); //2024-01-16 14:05:38
}
}
File 的创建和删除
方法名称 | 说明 |
---|---|
public boolean createNewFile() |
创建一个新的空的文件 |
public boolean mkdir() |
创建单级文件夹 |
public boolean mkdirs() |
创建多级文件夹 |
public boolean delete() |
删除文件、空文件夹(删了就没了,不会放入回收站) |
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//创建文件
File f1 = new File("D:\\HNU\\test.txt"); //这里的File是路径
boolean newFile = f1.createNewFile(); //调用createNewFile方法后才能根据路径创建出对应文件
System.out.println(newFile); //如果文件存在,则返回false,不存在,就会创建文件后返回true
//如果指定路径不存在,则会抛出异常
//如果没有标明后缀名,则会创建一个不带后缀名的空文件
//创建单级文件夹(目录)
File f2 = new File("D:\\HNU\\dir");
boolean mkdir = f2.mkdir();
System.out.println(mkdir);
//创建多级文件夹
File f3 = new File("D:\\HNU\\dir1\\dir2\\dir3");
boolean mkdirs = f3.mkdirs();
System.out.println(mkdirs);
//删除,无法删除非空文件夹
File f4 = new File("D:\\HNU\\dir1");
boolean delete = f4.delete();
System.out.println(delete);
}
}
删除多级文件如下:
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
// File file = new File("D:\\HNU\\dir\\dir1\\dir2\\dir3");
// file.mkdirs(); //创建一个多级文件
//现删除一个多级文件夹
deleteDirs(new File("D:\\HNU\\dir"));
}
public static void deleteDirs(File file) {
File[] files = file.listFiles();
if(files == null) return; //需要防止空指针异常
for (File f : files) {
if(f.isFile()) {
f.delete(); //是文件直接删除
}
else {
deleteDirs(f);
}
}
file.delete(); //再删掉自己
}
}
File 的获取和遍历
方法名称 | 说明 |
---|---|
public File[] listFiles() |
获取当前该路径下的所有内容 |
注意:
- 当调用者 File 表示的路径不存在时,返回 null。
- 当调用者 File 表示的路径是文件时,返回 null。
- 当调用者 File 表示的路径是一个空文件夹时,返回一个长度为0的数组。
- 当调用者 File 表示的路径是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在 File 数组中返回。
- 当调用者 File 表示的路径是一个有隐藏文件的文件夹时,将里面所有的文件和文件夹路径放在 File 数组里面,然后返回,包含隐藏文件。
- 当调用者 File 表示的路径是需要权限才能访问的文件夹时,返回 null。
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
File f = new File("D:\\HNU");
File[] files = f.listFiles();
for (File file : files) {
System.out.println(file);
}
}
}
还有其他获取并遍历的方法(了解即可):
方法名称 | 说明 |
---|---|
public static File[] listRoots() |
列出可用的文件系统根 |
public String[] list() |
获取当前该路径下的所有内容 |
public String[] list(FilenameFilter filter) |
利用文件名过滤器获取当前该路径下的所有内容(打印名称) |
public File[] listFiles(FileFilter filter) |
利用文件名过滤器获取当前该路径下的所有内容(打印路径) |
public File[] listFiles(FilenameFilter filter) |
利用文件名过滤器获取当前该路径下的所有内容(打印路径) |
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
//listRoots可以获取系统中所有的盘符
File[] arr = File.listRoots();
System.out.println(Arrays.toString(arr)); //[C:\, D:\]
//list获取当前该路径下的所有内容,但仅仅只能获取名字
File f1 = new File("D:\\HNU");
String[] list = f1.list();
for (String s : list) {
System.out.println(s);
}
//利用文件名过滤器的list
//现获取D:\HNU下的所有txt文件
File f2 = new File("D:\\HNU");
String[] arr2 = f2.list(new FilenameFilter() {
@Override
//dir表示父级路径 name表示子级路径
public boolean accept(File dir, String name) {
File src = new File(dir, name);
//如果返回值为true,则表示当前路径保留,反之则舍弃
return src.isFile() && name.endsWith(".txt");
}
});
System.out.println(Arrays.toString(arr2));
//不使用文件过滤器也可以实现上述功能
File f3 = new File("D:\\HNU");
File[] files = f3.listFiles();
for (File file : files) {
if(file.isFile() && file.getName().endsWith(".txt")) {
System.out.println(file);
}
}
//使用文件过滤器的listFiles
File[] files1 = f3.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isFile() && pathname.getName().endsWith(".txt");
}
});
System.out.println(Arrays.toString(files1));
File[] files2 = f3.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
File src = new File(dir, name);
return src.isFile() && name.endsWith(".txt");
}
});
System.out.println(Arrays.toString(files2));
}
}
IO流
IO流:存储和读取数据的解决方案。
数据如果存储在内存中,是不能永久性存储的,只有当数据保存在硬盘的文件中,才能将数据进行永久性存储。
读写时是以程序为参照看读写方向的。
- IO流按照流的方向分类可以分为:输入流(读取)、输出流(写出)。
- IO流按照操作文件类型可以分为:字节流(操作所有类型的文件,一般用来拷贝文件夹)、字符流(操作纯文本文件)。(纯文本文件:能够通过 windows 系统自带的记事本打得开并且读得懂的,例如:txt、md、xml、lrc文件等)
IO流的使用原则:随用随创建,什么时候要用,什么时候就创建,用完及时关闭。(因为如果对同一个文件提前创建输入输出流的话,输出流会把文件清空,导致输入流出错)
IO流的体系
flowchart TD id1(io流体系) id2(字节流) id3(字符流) id4(InputStream\n字节输入流) id5(OutputStream\n字节输出流) id6(Reader\n字符输入流) id7(Writer\n字符输出流) id1 --> id2 & id3 id2 --> id4 & id5 id3 --> id6 & id7
flowchart TD id1(字节流) id2(InputStream\n字节输入流) id3(OutputStream\n字节输出流) id4(FileInputStream\n操作本地文件的字节输入流) id5(FileOutputStream\n操作本地文件的字节输出流) id6(BufferedInputStream\n字节缓冲输入流) id7(BufferedOutputSteam\n字节缓冲输出流) id1 --> id2 & id3 id2 --> id4 --> id6 id3 --> id5 --> id7
flowchart TD id1(字符流) id2(Reader\n字符输入流) id3(Writer\n字符输出流) id4(FileReader\n操作本地文件的字符输入流) id5(FileWriter\n操作本地文件的字符输出流) id6(BufferedReader\n字符缓冲输入流) id7(BufferedWriter\n字符缓冲输出流) id1 --> id2 & id3 id2 --> id4 --> id6 id3 --> id5 --> id7
FileOutputStream
操作本地文件的字节输出流,可以把程序中的数据写到本地文件中。
书写步骤:
- 创建字节输出流对象。
- 写数据。
- 释放资源。
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//要求:写出一段文字到本地文件中
//创建对象
FileOutputStream fos = new FileOutputStream("D:\\HNU\\a.txt");
/*
* 1.参数是字符串表示的路径或者是File对象都是可以的
* 2.如果文件不存在,则会创建一个新的文件,但是需要保证父级路径是存在的
* 3.如果文件已经存在,则会覆盖文件
* */
//写出数据
fos.write(97); //写入'a'
/*
* write方法中填入的是整数,但是会按照ASCII码进行对应
* */
//释放资源
fos.close();
/*
* 1.每次使用流之后,都需要释放资源
* 2.不释放资源的话,java会一直占用文件资源
* */
}
}
FileOutputStream 写数据的方式:
方法名称 | 说明 |
---|---|
void wirte(int b) |
一次写一个字节数据 |
void wirte(byte[] b) |
一次写一个字节数组数据,利用 String 的 getBytes 方法可以获取到数组 |
void write(byte[] b, int off, int len) |
一次写一个字节数组的部分数据,第二个参数是起始索引,第三个参数是写的长度 |
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//要求:写出一段文字到本地文件中
//创建对象
FileOutputStream fos = new FileOutputStream("D:\\HNU\\a.txt");
//写出单个字符
fos.write(97);
//写出多个字符
byte[] bytes = {97, 98, 99, 100, 101};
fos.write(bytes);
//写出部分字符
fos.write(bytes, 2, 3); //99 100 101
//释放资源
fos.close();
}
}
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象,在第二个参数填入true,打开续写开关,可以对文件进行续写
FileOutputStream fos = new FileOutputStream("D:\\HNU\\a.txt", true);
//写出数据
String str = "testWords\r\ntest";
fos.write(str.getBytes());
//写出换行符便可换行
/*
* Windows: \r\n (\r是将光标挪到最前面,\n是将光标换到下一行)
* Linux: \n
* Max: \r
* 在java中,windows系统下会对\r或者\n进行补全,但是还是建议写完整
* */
//释放资源
fos.close();
}
}
FileInputStream
书写步骤:
- 创建字节输入流对象。
- 读数据。
- 释放资源。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
/*
* 如果文件不存在,则直接报错
* */
//读取数据
int read = fis.read();
System.out.println((char)read); //强转成char类型
/*
* 1.读出来的是对应的ASCII码
* 2.一次性读一个
* 3.如果读到了文件末尾,则read = -1
* */
//释放资源
fis.close();
}
}
FileInputStream 循环读取:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
//循环读取
int read; //这里需要定义一个变量进行辅助记录,因为read方法是读取数据,同时会移动指针
while((read = fis.read()) != -1) {
System.out.print((char)read);
}
//释放资源
fis.close();
}
}
字符集
字节流(FileOutputStream、FileInputStream)读取中文会出现乱码。
在计算机中,任意数据都是以二进制的形式来进行存储的。8个比特组合成一个字节,字节是计算机中存储的最小单位。存储英文时,只需要一个字节即可。
ASCII字符集:
一共128个字符,而一个字节最多存储256位信息,所以存储时只需要一个字节即可。当实际二进制不足8位时,高位补0。例如97的二进制是110 0001,存储时会存为0110 0001,补0的操作称为编码。因为是高位补0,解码时对实际没有影响,所以ASCII码的解码是直接解码,不需要额外处理,直接转成十进制即可。
GB2312字符集:
我国于1980年发布的适用于中文编码的字符集,全称为“中华人民共和国国家标准信息交换用汉字编码字符集”,其中GB为国家标准的意思。
该字符集一共收录了7445个图形字符,其中包括了6763个简体汉字。
BIG5字符集:
台湾地区繁体中文标准字符集,共收录13053个中文字,1984年实施。
GBK字符集:
2000年3月17日发布,收录了21003个汉字。包含国家标准GB13000-1的全部中日韩汉字,和BIG5编码中的所有汉字。其中K为扩展的意思。
GBK字符集中,英文用一个字节存储,是完全兼容ASCII码的。英文编码规则与ASCII码一致,不足8位则高位补0。
GBK字符集中,汉字的存储需要用到两个字节。例如存储汉字的“汉”字,查询GBK发现对应的编码为47802,转换为二进制是10111010 10111010,一共两个字节。其中,前8位称为高位字节,后8位称为低位字节。高位字节一定以1开头,高位字节转换成十进制之后是一个负数。这是为了与英文的编码方式区分开,英文的编码是高位补0,而中文要以最高位补1来进行区分。
GBK的解码无需任何操作,只需要将二进制直接转换为十进制即可。
ANSI:
Windows系统下有很多个版本,简体中文版默认使用的是GBK字符集,繁体中文版默认使用的是BIG5字符集,韩文版默认使用的是EUC-KR字符集,日文版默认使用的是Shift-JS字符集。微软为了方便管理,统一显示为ANSI。
Unicode字符集:
国际标准字符集(万国码),它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。研发方为统一码联盟(Unicode组织),于1990年研发,总部位于美国加州。
Unicode也是完全兼容ASCII字符集的。但是,对于编码来讲,Unicode字符集的编码方式则复杂得多。
最先提出来的,是UTF-16的编码方式(Unicode Transfer Format,最常用的转化为16个比特位),即利用2-4个字节进行存储。后来出现UTF-32编码方式,固定使用4个字节保存。
最后,提出UTF-8的编码规则,是可变的编码规则,使用1-4个字节保存,开发中最为常用。ASCII码用1个字节存储,中文使用3个字节存储。编码时ASCII高位补0。对于中文,第一个8位的前四位填写为1110,第二个8位的前两位填写为10,第三个8位的前两位填写为10,其余补充相对应的二进制码。
为什么会有乱码?
原因1:读取数据时未读取完整个汉字。例如利用字节流读取UTF-8文件的中文。UTF-8中,中文由3个字节组成,而字节流一次性只读一个字节,故会读取出错。
原因2:编码和解码的方式不统一。例如使用GBK的解码方式对UTF-8的中文进行解码,很显然,这会出错。
解决乱码的方式:不要用字节流读取文本文件,并且解码和编码的时候要用同一套形式。
FileReader
书写步骤如下:
- 创建对象。(如果文件不存在,则直接报错)
- 读取数据。(按字节读取,遇到中文则一次性读多个字节,最后解码返回一个整数)
- 释放资源。
FileReader会按照编译器默认的编码解码方式进行读取,所以就算可以一次性读多个字节,但是遇到解码译码方式不对等的情况下依旧会产生乱码,这个时候可以利用 Charset 的forName
方法指定FileReader的解码方式。(具体操作见“转换流”)
方法 | 说明 |
---|---|
public FileReader(File file) |
创建字符输入流关联本地文件 |
public FileReader(String pathname) |
创建字符输入流关联本地文件 |
public int read() |
读取数据,读到末尾返回-1 |
public int read(char[] buffer) |
读取多个数据,读到末尾返回-1 |
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
FileReader fr = new FileReader("D:\\HNU\\a.txt");
//读取数据
int ch;
while((ch = fr.read()) != -1) {
System.out.print((char)ch);
}
//释放资源
fr.close();
}
}
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
FileReader fr = new FileReader("D:\\HNU\\a.txt");
//读取数据
char[] chars = new char[2];
int len;
while((len = fr.read(chars)) != -1) {
System.out.print(new String(chars, 0, len)); //不需要手动强转
}
//释放资源
fr.close();
}
}
FileWriter
方法 | 说明 |
---|---|
public FileWriter(File file, boolean append) |
创建字符输出流关联本地文件,续写 |
public FileWriter(String pathname, boolean append) |
创建字符输出流关联本地文件,续写 |
void write(int c) |
写出一个字符 |
void write(String str) |
写出一个字符串 |
void write(String str, int off, int len) |
写出一个字符串的一部分 |
void write(char[] cbuf) |
写出一个字符数组 |
void write(char[] cbuf, int off, int len) |
写出字符数组的一部分 |
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
FileWriter fw = new FileWriter("D:\\HNU\\a.txt");
//写出字符串
String str = "写出测试\r\n";
fw.write(str);
//写出字符数组
char[] chars = {'a', 'b', 'c', '你', '好'};
fw.write(chars);
//释放资源
fw.close();
}
}
字符流原理
创建 FileReader 流对象之后,在内存中会开辟一个长度为8192的字节数组,作为缓冲区(字节流没有缓冲区,只有字符流才有)。在读文件的时候,会将文件当中的数据尽可能多的搬入到缓冲区中,然后再在缓冲区中对数据进行读取,尽量减少和文件的直接交互。
文件当中的数据搬入缓冲区时,会覆盖原本缓冲区的数据。
创建 FileWriter 流对象之后,会在内存中开辟一个长度为8192的字节数组,作为缓冲区。在写数据时,会先把数据写入缓冲区中。当缓冲区装满、调用flush
方法或者调用close
方法时会刷新缓冲区。
字节缓冲流
底层自带了长度为8192的缓冲区提高性能。实质是对基本流进行包装,实际做操作的,还是基本流。
方法名称 | 说明 |
---|---|
public BufferedInputStream(InputStream is) |
缓冲输入流的构造 |
public BufferedOutputStream(OutputStream os) |
缓冲输出流的构造 |
利用字节缓冲流拷贝文件:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
//创建缓冲流对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\HNU\\a.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\HNU\\b.txt"));
//循环读取并写入
int read;
while((read = bis.read()) != -1) {
bos.write(read);
}
//释放资源
bos.close();
bis.close();
}
}
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
//创建缓冲流对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\HNU\\a.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\HNU\\b.txt"));
//使用字节数组拷贝
byte[] bytes = new byte[1024];
int read;
while((read = bis.read(bytes)) != -1) {
bos.write(bytes, 0, read);
}
//释放资源
bos.close();
bis.close();
}
}
字符缓冲流
字符基本流内部已经带有缓冲区,但是字符缓冲流在基本流的基础上增加了几个较为常用的方法。因为 char 类型占两个字节大小,所以对于字符缓冲流的缓冲区来讲,其长度为 8192,故总大小应为16K。
方法名称 | 说明 |
---|---|
pulbic BufferedReader(Reader r) |
缓冲输入流的构造 |
public BufferedWriter(Writer r) |
缓冲输出流的构造 |
public String readLine() |
字符缓冲输入流特有方法,读取一行数据,如果没有数据可读了,便返回 null |
public void newLine() |
字符缓冲输出流特有方法,跨平台换行,指在不同操作系统下都可以换行 |
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
//创建缓冲流对象
BufferedReader br = new BufferedReader(new FileReader("D:\\HNU\\a.txt"));
//读取数据
//遇到回车换行便停止,不会捕获回车到内存当中
String line;
while((line = br.readLine()) != null) {
System.out.println(line);
}
//释放资源
br.close();
}
}
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
//创建缓冲流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\HNU\\b.txt"));
//写出数据
bw.write("缓冲流写出测试");
bw.newLine(); //使用newLine实现跨平台换行
bw.write("换行后测试");
//释放资源
bw.close();
}
}
转换流
转换流属于字符流,是一种高级流,有 InputStreamReader(转换输入流)和 OutputStreamWriter(转换输出流)。
转换流是字符流和字节流之间的桥梁。转换流使得字节输入输出流具有字符输入输出流的特点(读中文不会乱码,可以根据字符集读取多个字节)。
如果字节流想要使用字符流中的方法,则需要使用转换流进行转换。
import java.io.*;
import java.nio.charset.Charset;
public class Main {
public static void main(String[] args) throws IOException {
//利用转换流按照指定的字符编码读取数据
//创建对象并指定字符编码
// InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\HNU\\gbktest.txt"), "GBK");
// //读取数据
// int read;
// while((read = isr.read()) != -1) {
// System.out.print((char)read);
// }
// //释放资源
// isr.close();
//以上方法在JDK11后被淘汰,JDK11后可以使用下面的方法进行解码
FileReader fr = new FileReader("D:\\HNU\\gbktest.txt", Charset.forName("GBK"));
int read;
while((read = fr.read()) != -1) {
System.out.print((char)read);
}
fr.close();
}
}
import java.io.*;
import java.nio.charset.Charset;
public class Main {
public static void main(String[] args) throws IOException {
//利用转换流按照指定的字符编码写出数据
// OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\HNU\\b.txt"), "GBK");
// osw.write("转换流写出测试");
// osw.close();
FileWriter fw = new FileWriter("D:\\HNU\\b.txt", Charset.forName("GBK"));
//写出数据
fw.write("转换流写出测试");
//释放资源
fw.close();
}
}
利用转换流实现字节流、字符流之间的转化:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
//创建字节流对象
FileInputStream fis = new FileInputStream("D:\\HNU\\b.txt");
//字节流对象创建转换流对象
InputStreamReader isr = new InputStreamReader(fis);
//转化流对象可以读取中文,但是无法一整行的读,所以还需要转换成字符缓冲流对象
BufferedReader br = new BufferedReader(isr);
//读取数据
String str;
while((str = br.readLine()) != null) {
System.out.println(str);
}
//释放资源
br.close();
}
}
序列化流
属于字节流,是一种高级流,包括了ObjectInputStream(反序列化流)和 ObjectOutputStream(序列化流)。
它们负责将 java 中的对象信息与文件进行读写。
ObjectOutputStream:
使用序列化流将对象保存到文件时会出现 NotSerializableException 异常,需要让 Javabean 类实现 Serializable
接口。
Serializable
接口中是没有任何抽象方法的,属于标记型接口。一旦实现了该接口,意味着这个类可以被序列化。
方法 | 说明 |
---|---|
public ObjectOutputStream(OutputStream out) |
序列化流的构造 |
public final void writeObject(Object obj) |
将对象信息写入文件 |
import java.io.Serializable;
public class Student implements Serializable { //需要实现Serializable这个标记型接口
private String name;
private int age;
//...
}
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
Student stu = new Student("张三", 23);
//创建序列化流的对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\HNU\\a.txt"));
//写出数据
oos.writeObject(stu);
//释放资源
oos.close();
}
}
ObjectInputStream:
方法 | 说明 |
---|---|
public ObjectInputStream(InputStream out) |
反序列化流的构造 |
public Object readObject() |
将对象信息读取到程序当中 |
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//创建反序列化流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\HNU\\a.txt"));
//读取数据
Object object = ois.readObject();
//释放资源
ois.close();
//打印对象
System.out.println(object);
}
}
注意:
- 当我们使用旧的类信息写入文件后,更新了类的成员变量,这个时候,再利用文件读取类的信息,会抛出错误。原因是修改类的成员变量后类的版本号发生了改变,所以为了防止这种情况出现,我们可以手动定义一个版本号,版本号的名称固定为
serialVersionUID
。 - 如果我们不想要某个成员变量的属性被序列化到文件当中,我们可以使用
transient
关键字对成员变量进行修饰。
import java.io.Serial;
import java.io.Serializable;
public class Student implements Serializable {
@Serial
private static final long serialVersionUID = 4430830299345459386L;
private String name;
private int age;
private transient String address; //瞬态关键字,不会将成员变量序列化到文件当中
//...
}
在 idea 的设置中搜索 Serializable,而后勾选 JVM 语言中的选项即可。
序列化流读取多个对象:
import java.io.*;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//创建多个对象
//createObjects();
//读取多个对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\HNU\\a.txt"));
//读取数据
ArrayList<Student> objects =(ArrayList<Student>) ois.readObject();
//使用while循环读取多个对象信息时,会报出EOFException
//为了防止出异常,我们将多个对象存入集合中,读取集合
for(Student stu : objects) {
System.out.println(stu);
}
}
public static void createObjects() throws IOException {
//创建多个对象
Student s1 = new Student("zhangsan", 23, "南京");
Student s2 = new Student("lisi", 22, "北京");
Student s3 = new Student("wangwu", 23, "东京");
//将对象存入集合当中
ArrayList<Student> list = new ArrayList<>();
list.add(s1);
list.add(s2);
list.add(s3);
//创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\HNU\\a.txt"));
oos.writeObject(list);
//释放资源
oos.close();
}
}
打印流
打印流也是高级流,打印流不能读,只能写。其下有 PrintStream(字节打印流)和 PrintWriter(字符打印流)。
打印流只操作文件目的地,不操作数据源。并且打印流有特有的方法可以实现数据的原样写出。其特有的写出方法也可以实现自动刷新,自动换行。
换句话讲,打印流打印一次数据 = 写出 + 换行 + 刷新。
方法 | 说明 |
---|---|
public PrintStream(OutputStream/File/String) |
关联字节输出流/文件/文件路径 |
public PrintStream(String fileName, Charset charset) |
指定字符编码 |
public PrintStream(OutputStream out, boolean autoFlush) |
自动刷新(字节打印流底层没有缓冲区,开不开刷新都一样,字符打印流是有缓冲区的,效率更高) |
public PrintStream(OutputStream, boolean autoFlush, String encoding) |
指定字符编码且自动刷新 |
public void write(int b) |
常规方法:将指定的字节写出 |
public void println(Xxx xxx) |
特有方法:打印任意数据,自动刷新,自动换行 |
public void print(Xxx xxx) |
特有方法:打印任意数据,不换行 |
public void printf(String format, Object...args) |
特有方法:带有占位符的打印语句,不换行 |
import java.io.*;
import java.nio.charset.Charset;
public class Main {
public static void main(String[] args) throws IOException {
//创建字节打印流的对象
PrintStream ps = new PrintStream(new FileOutputStream("D:\\HNU\\a.txt"), true, Charset.forName("UTF-8"));
//写出数据
ps.println(97);
ps.println("你好" + 123 + "hello world");
ps.printf("%s 测试 %s", "文本1", "文本2");
//释放资源
ps.close();
//PrinteWriter同理,只不过在构造方法上有点小区别,一般不使用指定字符集的构造方法
//创建字符打印流对象
PrintWriter pw = new PrintWriter(new FileWriter("D:\\HNU\\a.txt"), true);
//记得打开true,不然不会刷新缓冲区
//写出数据
pw.println("今天你终于叫我名字了,虽然叫错了,但是没关系,我马上改");
//释放资源
pw.close();
}
}
在 System 类当中,封装了一个 PrintStream :public static final PrintStream out = null;
,其中,out 就被我们称为系统中的标准输出流。
这个流在系统中是唯一的,由虚拟机开启,是不能关闭的,否则将无法通过 out 打印数据到控制台上,除非重启虚拟机。
解压缩流、压缩流
属于字节流,解压缩流需要压缩包为 zip 形式。压缩包里的每一个文件,在 Java 当中都是一个ZipEntry
对象,所以解压的本质就是把每一个ZipEntry
对象按照层级拷贝到本地另一个文件当中。
利用 getNextEntry
方法可以获得压缩包当中的压缩文件对象。会自动获取所有文件夹和文件,读到最后返回 null。
利用 putNextEntry
方法可以将压缩文件对象写入文件当中,但是要记得把文件内容也拷贝进去。
ZipInputStream 解压缩流:
import java.io.*;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class Main {
public static void main(String[] args) throws IOException {
//创建一个File表示要解压的压缩包
File src = new File("D:\\HNU\\dir.zip");
//创建一个File表示要解压的目的地
File dest = new File("D:\\HNU\\");
//解压
unzip(src, dest);
}
//定义方法用来解压
public static void unzip(File src, File dest) throws IOException {
//解压就是把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地中
//创建一个解压缩流用来读取压缩包当中的数据
ZipInputStream zis = new ZipInputStream(new FileInputStream(src));
//读取数据,获取ZipEntry对象
ZipEntry nextEntry;
while((nextEntry = zis.getNextEntry()) != null) {
//System.out.println(nextEntry);
//读取到ZipEntry之后
if(nextEntry.isDirectory()) {//对于文件夹:需要在目的地dest处创建一个同样的文件夹
File file = new File(dest, nextEntry.toString()); //父级是dest,子级是ZipEntry的路径
file.mkdirs();
}
else {
//对于文件:需要读取到压缩包中的文件,并把它存放到目的地dest文件夹当中(按照层级目录进行存放)
File file = new File(dest, nextEntry.toString());
//拷贝数据
FileOutputStream fos = new FileOutputStream(file);
int read;
while((read = zis.read()) != -1) {
fos.write(read);
}
//释放资源
fos.close();
//closeEntry表示我读取一次压缩信息的结束
zis.closeEntry();
}
}
//释放资源
zis.close();
}
}
ZipOutputStream 压缩流:
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class Main {
public static void main(String[] args) throws IOException {
//将a.txt压缩打包(一个文件的压缩)
//创建File对象表示要压缩的文件
File src = new File("D:\\HNU\\a.txt");
//创建File对象表示压缩包的位置
File dest = new File("D:\\HNU\\");
//调用方法用来压缩
tozip(src, dest);
}
public static void tozip(File src, File dest) throws IOException { //参数一表示要压缩的文件,参数二表示压缩包的位置
//创建压缩流
File file = new File(dest, "a.zip"); //把文件压缩至dest的a.zip中
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file));
//创建ZipEntry对象,表示压缩包里面的每一个文件和文件夹
ZipEntry entry = new ZipEntry("a.txt");
//把ZipEntry对象放入压缩包当中
zos.putNextEntry(entry);
//把src文件内容写入压缩包当中
FileInputStream fis = new FileInputStream(src);
int read;
while((read = fis.read()) != -1) {
zos.write(read);
}
//释放资源
fis.close();
zos.close();
}
}
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class Main {
public static void main(String[] args) throws IOException {
//将文件夹压缩成压缩包
//创建File对象表示要压缩的文件夹
File src = new File("D:\\HNU\\dir");
//创建File对象表示压缩包的路径
File destParent = src.getParentFile(); //获得根目录
File dest = new File(destParent, src.getName() + ".zip");
//创建压缩流关联压缩包
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
//获取src当中的每一个文件,变成ZipEntry对象,放入压缩包当中
tozip(src, zos, src.getName());
//释放资源
zos.close();
}
public static void tozip(File src, ZipOutputStream zos, String name) throws IOException {
/*
* 作用:获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包中
* 参数一:数据源
* 参数二:压缩流
* 参数三:压缩包的内部路径
* */
//进入src文件夹
File[] files = src.listFiles();
for (File f : files) {
if(f.isFile()) {
//如果是文件,则放入压缩对象当中
ZipEntry entry = new ZipEntry(name + "\\" + f.getName());
zos.putNextEntry(entry);
//写入压缩包
FileInputStream fis = new FileInputStream(f);
int read;
while((read = fis.read()) != -1) {
zos.write(read);
}
fis.close();
zos.closeEntry();
}
else {
tozip(f, zos, name + "\\" + f.getName());
}
}
}
}
Java中编码和解码的代码实现
Java 中的编码方法:
String类中的方法 | 说明 |
---|---|
public byte[] getBytes() |
使用默认方法进行编码 |
public byte[] getBytes(String charsetName) |
使用指定方式进行编码 |
Java中的解码方法:
String类中的方法 | 说明 |
---|---|
String(byte[] bytes) |
使用默认方式进行解码 |
String(byte[] bytes, String charsetName) |
使用指定方式进行解码 |
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws UnsupportedEncodingException {
//编码
String str = "test文本";
byte[] bytes1 = str.getBytes(); //idea默认使用utf-8的编码方式
System.out.println(Arrays.toString(bytes1)); //[116, 101, 115, 116, -26, -106, -121, -26, -100, -84]
byte[] bytes2 = str.getBytes("GBK"); //指定使用GBK方式进行编码
System.out.println(Arrays.toString(bytes2)); //[116, 101, 115, 116, -50, -60, -79, -66]
//解码
String str2 = new String(bytes1); //使用utf-8解码
System.out.println(str2); //test文本
String str3 = new String(bytes2); //使用utf-8解码GBK
System.out.println(str3); //test�ı� 产生了乱码
String str4 = new String(bytes2, "GBK"); //使用GBK解码GBK
System.out.println(str4); //test文本
}
}
文件的拷贝
小文件的拷贝:小文件的拷贝只需要用 while 循环暴力复制就好,边读边写。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt");
//拷贝,边读边写
int read;
while((read = fis.read()) != -1) {
fos.write(read);
}
//释放资源,先开的流最后再关闭
fos.close();
fis.close();
}
}
大文件拷贝:大文件的拷贝需要我们一次性多读几个字节,需要在 read 方法中传递 byte 数组,数组长度一般为1024 的整数倍,可以设置为 1024 * 1024 * 5 (大小为 5MB 的数组,读一次拷贝 5MB 的内容)。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
//读取数据
byte[] bytes = new byte[2];
//read表示本次读取到多少个字节数据
int read = fis.read(bytes);
System.out.println(read); //2
String str = new String(bytes, 0, read); //使用0, read来确保读入正确
System.out.println(str); //将读入的数据存储到byte数组中
//读取时会覆盖原本byte的内容,所以如果最后读取的长度小于数组本身长度,则会保留一部分上一次读取的内容
//读不到数据则返回-1
//释放资源
fis.close();
}
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt");
//读取数据,一次性读取5MB
byte[] bytes = new byte[1024 * 1024 * 5];
//read表示本次读取到多少个字节数据
int read;
while((read = fis.read(bytes)) != -1) {
fos.write(bytes, 0, read);
}
//释放资源
fos.close();
fis.close();
}
}
文件夹的拷贝:
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
//创建对象
File src = new File("D:\\HNU\\dir\\src"); //源文件夹
File dest = new File("D:\\HNU\\dir\\dest"); //目的文件夹
//开始拷贝
copyDir(src, dest);
}
public static void copyDir(File src, File dest) throws IOException { //数据源,目的地
dest.mkdirs(); //如果不存在,则创建文件(用于拷贝嵌套文件夹)
//进入数据源
File[] files = src.listFiles();
if(files == null) {
//此处可以加上一个非空判断
}
//遍历数组
for (File file : files) {
if(file.isFile()) { //是文件,直接拷贝
//使用字节流进行拷贝
FileInputStream fis = new FileInputStream(file);
//数据是从文件开始,到文件结束
FileOutputStream fos = new FileOutputStream(new File(dest, file.getName()));
//使用数组进行拷贝
byte[] bytes = new byte[1024];
int read;
while((read = fis.read(bytes)) != -1) {
fos.write(bytes, 0, read);
}
//释放资源
fos.close();
fis.close();
}
else { //是文件夹,递归
copyDir(file, new File(dest, file.getName())); //将数据拷贝到文件夹里面
}
}
}
}
文件的加密
为了保证文件的安全性,就需要对原始文件进行加密存储,再使用的时候再对其进行解密处理。
加密原理:对原视文件中的每一个字节数据进行更改,然后将更改以后的数据存储到新的文件夹当中。
解密原理:读取加密之后的文件,按照加密的规则反向操作,变成原始文件。
接下来利用异或操作对文件进行加密,演示如下:
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
// encrypt(); //文件加密
decrypt(); //文件解密
}
public static void encrypt() throws IOException {
//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt");
//加密处理
int read;
while((read = fis.read()) != -1) {
fos.write(read ^ 1); //异或处理
}
//释放资源
fos.close();
fis.close();
}
public static void decrypt() throws IOException {
//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\b.txt");
FileOutputStream fos = new FileOutputStream("D:\\HNU\\c.txt");
//解密
int read;
while((read = fis.read()) != -1) {
fos.write(read ^ 1); //异或处理
}
//释放资源
fos.close();
fis.close();
}
}
文件数据的修改
文本文件中有如下数据:2-1-9-4-7-8
现需要对数字进行排序处理:
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
//读取数据
FileReader fr = new FileReader("D:\\HNU\\a.txt");
StringBuilder sb = new StringBuilder();
int read;
while((read = fr.read()) != -1) {
sb.append((char)read);
}
//释放资源
fr.close();
//排序
String str = sb.toString();
String[] arr = str.split("-");
Integer[] integers = Arrays
.stream(arr)
.map(Integer::parseInt) //字符串转换成整数
.sorted() //排序
.toArray(Integer[]::new);
//排序后的结果写出
String sj = Arrays.toString(integers); //[1, 2, 4, 7, 8, 9]
sj = sj.replace(", ", "-");
FileWriter fw = new FileWriter("D:\\HNU\\b.txt");
fw.write(sj.substring(1, sj.length() - 1));
fw.close();
}
}
常用工具包(Commons-io)
Commons-io 是 apache 开源基金组织(专门为支持开源软件项目而办的非盈利性组织)提供的一组有关 IO 操作的开源工具包。其作用是提高 IO 流的开发效率。
Commons-io使用步骤:
- 在项目中创建一个文件夹:lib。
- 将 jar 包复制粘贴到 lib 文件夹。
- 右键点击 jar 包,选择 Add as Library,点击 OK。
常见方法:
FileUtils类(文件、文件夹相关) | 说明 |
---|---|
static void copyFile(File srcFile, File destFile) |
复制文件 |
static void copyDirectory(File srcDir, File destDir) |
复制文件夹 |
static void copyDirectoryToDirectory(File srcDir, File destDir) |
复制文件夹 |
static void deleteDirectory(File directory) |
删除文件夹 |
static void cleanDirectory(file directory) |
清空文件夹 |
static String readFileToString(File file, Charset encoding) |
读取文件中的数据变成字符串 |
static void write(File file, CharSequence data, String encoding) |
写出数据 |
IOUtils类(流相关) | 说明 |
---|---|
public static int copy(InputStream input, OutputStream output) |
复制文件 |
public static int copyLarge(Reader input, Writer output) |
复制大文件 |
public static String readLines(Reader input) |
读取数据 |
public static void wirte(String data, OutputStream output) |
写出数据 |
import org.apache.commons.io.FileUtils;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
//复制文件
// File src = new File("D:\\HNU\\a.txt");
// File dest = new File("D:\\HNU\\b.txt");
// FileUtils.copyFile(src, dest);
//复制文件夹
// File src = new File("D:\\HNU\\dir");
// File dest = new File("D:\\HNU\\copyDir");
// FileUtils.copyDirectory(src, dest);
//删除文件夹
// File src = new File("D:\\HNU\\copyDir");
// FileUtils.deleteDirectory(src);
//清空文件夹
File src = new File("D:\\HNU\\copyDir");
FileUtils.cleanDirectory(src);
}
}
常用工具包(Hutool)
又称糊涂包,由我国程序员开发。
相关类 | 说明 |
---|---|
IoUtil |
流操作工具类 |
FileUtil |
文件读写和操作的工具类 |
FileTypeUtil |
文件类型判断工具类 |
WatchMonitor |
目录、文件监听 |
ClassPathResource |
针对 ClassPath 中资源的访问封装 |
FileReader |
封装文件读取 |
FileWriter |
封装文件写入 |
FileUtil
常用方法:
方法 | 说明 |
---|---|
touch() |
根据参数创建文件,就算父级路径不存在,也会一起把父级路径创建出来 |
writeLines(List<?> list, File file, String charset, boolean b) |
把集合中的数据写到文件当中,第四个参数可以设置是否续写,默认调用对象的toString 方法写入 |
readLines(File file, String charset) |
把文件中的数据读取到集合当中 |
readUtf8Lines(String path) |
使用utf-8方式读取path的内容到集合中 |
import cn.hutool.core.io.FileUtil;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException {
//根据参数创建File对象
File file = FileUtil.file("D:\\", "HNU", "b.txt"); //可变参数
System.out.println(file); // D:\HNU\b.txt
//touch:根据参数创建文件,就算父级路径不存在,也会一起把父级路径创建出来
FileUtil.touch(new File("D:\\HNU\\huToolTestDir\\test1\\b.txt"));
//writeLine:把集合中的数据写到文件当中,第四个参数可以设置是否续写
ArrayList<String> list = new ArrayList<>();
list.add("测试文本1");
list.add("测试文本2");
list.add("测试文本3");
File file = new File("D:\\HNU\\a.txt");
FileUtil.writeLines(list, file, "UTF-8", true); //打开续写
//readLines:把文件中的数据读取到集合当中
File file = new File("D:\\HNU\\a.txt");
List<String> list = FileUtil.readLines(file, "UTF-8");
System.out.println(list);
}
}
properties 配置文件的基本使用
配置文件:可以简单理解为软件的基础设置,有了配置文件,我们便可以永久保存软件的设置情况。如果我们要修改参数,不需要改动代码,只需要修改配置文件就可以了。
properties 文件中的数据是以键值对的形式存储的。在 Java 中,properties 也是一个双列集合,拥有 Map 集合的特点。具有一些特定的方法,可以把集合中的数据按照键值对的形式写道配置文件中。也可以把配置文件中的数据读取到集合中来。properties不是泛型类!
properties 的基本操作:
import java.util.Map;
import java.util.Properties;
import java.util.Set;
public class Main {
public static void main(String[] args) {
//创建集合对象
Properties prop = new Properties();
//添加数据
//虽然我们可以往Properties中添加任意数据,但是一般我们只会往里面添加字符串数据
prop.put("aaa", "111");
prop.put("bbb", "222");
prop.put("ccc", "333");
//遍历集合
Set<Object> keys = prop.keySet();
for (Object key : keys) {
Object value = prop.get(key);
System.out.println(key + " = " + value);
}
System.out.println("-----");
Set<Map.Entry<Object, Object>> entries = prop.entrySet();
for (Map.Entry<Object, Object> entry : entries) {
Object key = entry.getKey();
Object value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
properties 关于 IO 流的操作:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
public class Main {
public static void main(String[] args) throws IOException {
//创建集合对象
Properties prop = new Properties();
//读取数据
FileInputStream fis = new FileInputStream("D:\\HNU\\save.properties");
prop.load(fis);
fis.close();
//打印集合
System.out.println(prop);
}
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
public class Main {
public static void main(String[] args) throws IOException {
//创建集合对象
Properties prop = new Properties();
//添加数据
prop.put("aaa", "111");
prop.put("bbb", "222");
prop.put("ccc", "333");
prop.put("ddd", "444");
//把集合中的数据以键值对的形式写到本地文件中
FileOutputStream fos = new FileOutputStream("D:\\HNU\\save.properties");
//利用store方法将数据进行保存,第二个参数是文件的注释信息
prop.store(fos, "properties test");
fos.close();
}
}
不同的 JDK 版本捕获异常的方式
了解即可,实际开发中碰到异常都是抛出处理。
核心问题是需要保证即使出现异常的情况下,也要保证能够释放资源。
finally {
//finally中释放资源
if(fos != null) { //防止路径不存在而报出空指针异常
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Java 在 JDK7 时推出了接口 AutoCloseable
,可以在特定情况下自动释放资源。
基本做法:
try {
//可能出现异常的代码
}
catch (异常类名 变量名) {
//异常的处理代码
}
finally {
//执行所有资源释放操作
}
JDK7 方案:
//格式如下:
try(创建流对象1; 创建流对象2) { //只有实现了AutoCloseable接口类,才能在小括号中创建对象
//可能出现异常的代码
}
catch(异常类名 变量名) {
//异常的处理代码
}
//创建对象
try(FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt")) {
//读取数据,一次性读取5MB
byte[] bytes = new byte[1024 * 1024 * 5];
//read表示本次读取到多少个字节数据
int read;
while((read = fis.read(bytes)) != -1) {
fos.write(bytes, 0, read);
}
} catch (IOException e) {
e.printStackTrace();
}
JDK9 方案:
//格式如下:
创建流对象1;
创建流对象2;
try(流1; 流2) {
//可能出现异常的代码
}
catch(异常类名 变量名) {
//异常的处理代码
}
//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt");
try(fis; fos) {
//读取数据,一次性读取5MB
byte[] bytes = new byte[1024 * 1024 * 5];
//read表示本次读取到多少个字节数据
int read;
while((read = fis.read(bytes)) != -1) {
fos.write(bytes, 0, read);
}
} catch (IOException e) {
e.printStackTrace();
}
IO 流的综合练习
制造假数据
制造假数据也是开发当中的一个能力,在各个网站上面爬取数据,是其中一个办法。
现利用网络爬虫,爬取上述数据后,获得形如名字-性别-年龄
的数据,并写入文件中。
import java.io.*;
import java.util.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) throws IOException {
//定义变量用来记录网址
String familyName = "https://hanyu.baidu.com/shici/detail?pid=0b2f26d4c0ddb3ee693fdb1137ee1b0d&from=kg0";
String boyNameNet = "http://www.haoming8.cn/baobao/10881.html";
String girlNameNet = "http://www.haoming8.cn/baobao/7641.html";
//爬取数据,把网址上所有的数据拼接成一个字符串
String familyNameStr = webCrawler(familyName);
String boyNameStr = webCrawler(boyNameNet);
String girlNameStr = webCrawler(girlNameNet);
//通过正则表达式把其中符合要求的数据获取出来
//通过anyRule查询,得知单个中文汉字的正则表达式是:[\u4E00-\u9FA5]
//姓氏的规则是:4个汉字后面跟逗号或句号
ArrayList<String> familyNameTempList = getData(familyNameStr, "[\\u4E00-\\u9FA5]{4}(?=,|。)");
//男生名字的规则是:两个汉字后面跟顿号或句号
ArrayList<String> boyNameTempList = getData(boyNameStr, "[\\u4E00-\\u9FA5]{2}(?=、|。)");
//女生名字的规则是:5个由两个汉字组成的名字中间使用4个空格隔开
ArrayList<String> girlNameTempList = getData(girlNameStr, "([\\u4E00-\\u9FA5]{2} ){4}[\\u4E00-\\u9FA5]{2}");
//处理数据
//姓氏的处理方案:把每一个姓氏拆开放入集合中
ArrayList<String> familyNameList = new ArrayList<>();
for (String str : familyNameTempList) {
for(int i = 0; i < str.length(); ++i) {
familyNameList.add(String.valueOf(str.charAt(i)));
}
}
//男生名字处理方案:去重
ArrayList<String> boyNameList = new ArrayList<>(boyNameTempList.stream().distinct().toList());
//女生名字处理方案
ArrayList<String> girlNameList = new ArrayList<>();
for (String str : girlNameTempList) {
String[] strArr = str.split(" ");
girlNameList.addAll(Arrays.asList(strArr));
}
//生成数据:姓名(唯一)-性别-年龄
ArrayList<String> infos = getInfos(familyNameList, boyNameList, girlNameList, 10, 10);
//写出数据
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\HNU\\a.txt"));
for (String info : infos) {
bw.write(info);
bw.newLine();
}
//释放资源
bw.close();
}
/*
* 作用:从网络上爬取数据,把数据拼接成字符串返回
*
* 形参:网址
* 返回值:爬取到的所有数据
* */
public static String webCrawler(String src) throws IOException {
StringBuilder sb = new StringBuilder();
//创建URL对象
URL url = new URL(src);
//连接网址,需要保证网络畅通,并且保证这个网址是可以连接上的
URLConnection conn = url.openConnection();
//读取数据,利用转换流转成字符流
InputStreamReader isr = new InputStreamReader(conn.getInputStream());
int ch;
while((ch = isr.read()) != -1) {
sb.append((char)ch);
}
//释放资源
isr.close();
return sb.toString();
}
/*
* 作用:根据正则表达式获取字符串中的数据
* 参数一:完整的字符串
* 参数二:正则表达式
* 参数三:
* 返回值:真正想要的数据
* */
public static ArrayList<String> getData(String src, String regex) {
ArrayList<String> list = new ArrayList<>();
//按照正则表达式的规则获取数据,使用本地爬虫
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(src);
while(m.find()) {
list.add(m.group());
}
return list;
}
/*
* 作用:获取男生和女生的信息
* 格式:姓名(唯一)-性别-年龄
* 例如:张三-男-18
* 参数一:装着姓氏的集合
* 参数二:装着男生名字的集合
* 参数三:装着女生名字的集合
* 参数四:男生的个数
* 参数五:女生的个数
* */
public static ArrayList<String> getInfos(ArrayList<String> familyNameList,
ArrayList<String> boyNameList,
ArrayList<String> girlNameList,
int boyCount, int girlCount) {
//生成随机数
Random r = new Random();
//生成姓名
HashSet<String> boys = new HashSet<>();
while(boys.size() != boyCount) {
int familyNameRandom = r.nextInt(familyNameList.size());
int boyNameRandom = r.nextInt(boyNameList.size());
boys.add(familyNameList.get(familyNameRandom) + boyNameList.get(boyNameRandom));
}
HashSet<String> girls = new HashSet<>();
while(girls.size() != girlCount) {
int familyNameRandom = r.nextInt(familyNameList.size());
int girlNameRandom = r.nextInt(girlNameList.size());
girls.add(familyNameList.get(familyNameRandom) + boyNameList.get(girlNameRandom));
}
//生成最终数据
ArrayList<String> list = new ArrayList<>();
for (String boyName : boys) {
int age = r.nextInt(10) + 18;
StringJoiner sj = new StringJoiner("-");
sj.add(boyName);
sj.add("男");
sj.add(String.valueOf(age));
list.add(sj.toString());
}
for (String girlName : girls) {
int age = r.nextInt(10) + 18;
StringJoiner sj = new StringJoiner("-");
sj.add(girlName);
sj.add("女");
sj.add(String.valueOf(age));
list.add(sj.toString());
}
//打乱数据,提高随机性
Collections.shuffle(list);
return list;
}
}
利用 Hutool 包制造假数据
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.http.HttpUtil;
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
//定义变量用来记录网址
String familyNameNet = "https://hanyu.baidu.com/shici/detail?pid=0b2f26d4c0ddb3ee693fdb1137ee1b0d&from=kg0";
String boyNameNet = "http://www.haoming8.cn/baobao/10881.html";
String girlNameNet = "http://www.haoming8.cn/baobao/7641.html";
//爬取数据,使用HttpUtil可以爬取
String familyName = HttpUtil.get(familyNameNet);
String boyName = HttpUtil.get(boyNameNet);
String girlName = HttpUtil.get(girlNameNet);
//利用正则表达式获取数据
ArrayList<String> familyNameList = new ArrayList<>(ReUtil.findAll("[\\u4E00-\\u9FA5]{4}(?=,|。)", familyName, 0));
ArrayList<String> boyNameList = new ArrayList<>(ReUtil.findAll("[\\u4E00-\\u9FA5]{2}(?=、|。)", boyName, 0));
ArrayList<String> girlNameList = new ArrayList<>(ReUtil.findAll("([\\u4E00-\\u9FA5]{2} ){4}[\\u4E00-\\u9FA5]{2}", girlName, 0));
//处理数据
//...这里不再赘述
//写出数据
FileUtil.writeLines(list, "D:\\HNU\\a.txt", "UTF-8");
}
}
随机点名器(带权重的随机算法)
实现一款随机点名器,学生数据示例如下:姓名-性别-年龄-权重(默认为1)
。要求每次被点到的学生,再次被点到的概率在原先的基础上降低一半。
算法的思想在于先给每一个人分配一个权重占比,然后被点到的话就将这个权重占比下降一半。可以参考密度函数的思想,每一个人根据占比生成一个被点到的区间,每一次点名时生成一个随机数,当这个随机数落在这个区间中时,便表示这个人被点到名字。
public class Student {
private String name;
private String gender;
private int age;
private double weight; //每个学生的权重
//...
public String toString() { //重写toString方法,方便后续的文件写出
return name + "-" + gender + "-" + age + "-" + weight;
}
}
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
//把文件中所有的学生信息读取到内存中
ArrayList<Student> list = new ArrayList<>();
BufferedReader br = new BufferedReader(new FileReader("D:\\HNU\\a.txt"));
String line;
while ((line = br.readLine()) != null) {
String[] arr = line.split("-");
Student stu = new Student(arr[0], arr[1], Integer.parseInt(arr[2]), Double.parseDouble(arr[3]));
list.add(stu);
}
br.close();
//计算权重总和
double weight = 0;
for (Student stu : list) {
weight += stu.getWeight();
}
//计算出每一个人的实际占比
// 1/10 = 0.1
double[] arr = new double[list.size()];
for (int i = 0; i < arr.length; ++i) {
arr[i] = list.get(0).getWeight() / weight;
}
//计算每一个人的权重占比(0.0-0.1] (0.1-0.2] (0.2-0.3]...
for (int i = 1; i < arr.length; ++i) {
arr[i] = arr[i - 1] + arr[i];
}
//随机抽取
//利用Math的random方法获取一个0.0-1.0的随机数
double number = Math.random();
//判断number在arr中的范围,利用二分查找找出索引,方法返回 -插入点-1
int res = -Arrays.binarySearch(arr, number) - 1;
Student stu = list.get(res);
System.out.println(stu);
//调整占比
double w = stu.getWeight() / 2;
stu.setWeight(w);
//把集合的数据再一次写入集合中
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\HNU\\a.txt"));
for (Student student : list) {
bw.write(student.toString());
bw.newLine();
}
bw.close();
}
}
反射
反射允许对封装类的字段(成员变量),方法和构造函数的信息进行编程访问。
编译器当中自动提示的那些方法或者参数就是利用反射来得到,然后展示出来的。换句话讲,反射就是从类当中拿东西。
反射的作用:
- 获取一个类里面的所有信息,获取到了之后,再执行其他的业务逻辑。
- 结合配置文件,动态的创建对象并调用方法。
RTTI 在编译期知道要解析的类型,反射在运行期知道要解析的类型。
获取 class 对象
Class.forName("全类名")
类名.class
对象.getClass();
仅使用.class
获取类的引用并不会发生初始化,但是通过Class.forName()
的方式会进行初始化。
第一阶段:即源代码阶段,编写 .java 文件并编译为 .class 文件,这个阶段是在硬盘当中进行的,一般使用 Class.forName("全类名")
来获取 class 对象。
第二阶段:即加载阶段,运行程序时将 .class 文件加载到内存中,一般使用 类名.class
获取 class 对象。
第三阶段:即运行阶段,我们利用 new 关键字创建对象之后, 一般使用 对象.getClass()
来获取 class 对象。
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
//第一种方式 全类名:包名+类名
//全类名的获取方式:右键类名 -> 复制粘贴特殊 -> 复制引用
//最为常用
Class clazz = Class.forName("Student");
System.out.println(clazz); //class Student
//第二种方式
//一般当作参数传递
Class clazz1 = Student.class;
System.out.println(clazz1); //class Student
System.out.println(clazz == clazz1); //true
//第三种方式
//当我们已经有了这个类的对象后才可以使用
Student student = new Student();
Class clazz2 = student.getClass();
System.out.println(clazz2); //class Student
System.out.println(clazz1 == clazz2); //true
}
}
反射获取构造方法
在 Java 中,一切皆为对象,其中,Constructor 是构造方法类,用于接受构造方法;Field 是字段类,用于接受成员变量;Method 是方法类,用于接受成员方法。
Class 类中用于获取构造方法的方法:
方法名 | 说明 |
---|---|
Constructor<?>[] getConstructors() |
返回所有公共构造方法对象的数组(不包括私有) |
Constructor<?>[] getDeclaredConstructors() |
返回所有构造方法对象的数组(包括私有) |
Constructor<T> getConstructor(Class<?>...parameterTypes) |
返回单个公共构造方法对象(不包括私有) |
Constructor<T> getDeclaredConstructor(Class<?>...parameterTypes) |
返回单个构造方法对象(包括私有) |
Constructor 类中用于创建对象的方法
方法名 | 说明 |
---|---|
T newInstance(Object...initargs) |
根据指定的构造方法创建对象 |
setAccessible(boolean flag) |
设置为true,表示取消访问检查 |
int getParameterCount() |
获取参数个数 |
Parameter[] getParameters() |
获取全部参数 |
int getModifiers() |
获取权限修饰符(利用数字表示修饰符) |
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
public class Main {
public static void main(String[] args) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException,
InstantiationException, IllegalAccessException {
//获取class字节码文件对象
Class clazz = Class.forName("Student");
//获取构造方法
Constructor[] constructors = clazz.getConstructors();
for(Constructor constructor : constructors) {
System.out.println(constructor);
}
//public Student()
//public Student(java.lang.String)
//获取所有构造方法
Constructor[] constructors1 = clazz.getDeclaredConstructors();
for(Constructor constructor : constructors1) {
System.out.println(constructor);
}
//public Student()
//private Student(int,java.lang.String)
//protected Student(int)
//public Student(java.lang.String)
//获取单个方法
Constructor con = clazz.getDeclaredConstructor();
System.out.println(con); //public Student()
Constructor con1 = clazz.getDeclaredConstructor(String.class); //传递String的字节码文件
System.out.println(con1); //public Student(java.lang.String)
Constructor con2 = clazz.getDeclaredConstructor(int.class);
System.out.println(con2); //protected Student(int)
Constructor con3 = clazz.getDeclaredConstructor(int.class, String.class);
System.out.println(con3); //private Student(int,java.lang.String)
//获取权限修饰符
//public - 1 private - 2 protected - 4 static - 8
int modifiers = con3.getModifiers();
System.out.println(modifiers); //2
//获取参数个数
int parameterCount = con3.getParameterCount();
System.out.println(parameterCount); //2
//获取所有参数
Parameter[] parameters = con3.getParameters();
for(Parameter parameter : parameters) {
System.out.println(parameter);
}
//int arg0
//java.lang.String arg1
//利用构造方法创建对象
con3.setAccessible(true); //临时取消private的限制,否则下方两句代码无法运行,此之谓暴力反射
Student student = (Student) con3.newInstance(18, "张三");
System.out.println(student); //Student{age = 18, name = 张三}
}
}
反射获取成员变量
Class 类中用于获取成员变量的方法:
方法名 | 说明 |
---|---|
Field[] getFields() |
返回所有公共成员变量对象数组 |
Field[] getDeclaredFields() |
返回所有成员变量对象的数组 |
Field getField(String name) |
返回单个公共成员变量对象 |
Field getDeclaredField(String name) |
返回单个成员变量对象 |
Field 类中用于创建对象的方法:
方法名 | 说明 |
---|---|
void set(对象, Object value) |
赋值 |
Object get(Object obj) |
获取值 |
int getModifiers() |
获取修饰符 |
String getName() |
获取变量名称 |
Class<?> getType() |
获取变量属性 |
Object get(对象) |
获取对象此字段的属性值 |
setAccessible(boolean flag) |
设置为true,表示取消访问检查 |
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
public class Main {
public static void main(String[] args) throws ClassNotFoundException,
NoSuchFieldException, IllegalAccessException {
//获取class字节码文件对象
Class clazz = Class.forName("Student");
//获取公共成员变量
Field[] fields = clazz.getFields();
for(Field field : fields) {
System.out.println(field);
}
//public int Student.age
//public java.lang.String Student.name
//获取所有成员变量
Field[] fields1 = clazz.getDeclaredFields();
for(Field field : fields1) {
System.out.println(field);
}
//public int Student.age
//public java.lang.String Student.name
//private int Student.score
//获取单个成员变量
Field fld1 = clazz.getDeclaredField("score");
System.out.println(fld1); //private int Student.score
//获取成员变量的权限
int modifiers = fld1.getModifiers();
System.out.println(modifiers); //2
//获取成员变量名
String name = fld1.getName();
System.out.println(name); //score
//获取属性类型
Class<?> type = fld1.getType();
System.out.println(type); //int
//获取成员变量的值
Student student = new Student("张三");
Field fld2 = clazz.getDeclaredField("name");
Object value = fld2.get(student);
System.out.println(value); //张三
//修改成员变量的值
fld2.set(student, "李四");
System.out.println(student); //Student{age = 0, name = 李四}
}
}
反射获取成员方法
Class 类中用于获取成员方法的方法:
方法名 | 说明 |
---|---|
Method[] getMethod() |
返回所有公共成员方法对象的数组,包括继承的 |
Method[] getDeclaredMethods() |
返回所有成员方法对象的数组,不包括继承的 |
Method getMethod(String name, Class<?>...parameterTypes) |
返回单个公共成员方法对象 |
Method getDeclaredMethod(String name, Class<?>...parameterTypes) |
返回单个成员方法对象 |
Method 类中用于创建对象的方法:
方法名 | 说明 |
---|---|
Object invoke(Object obj, Object...args) |
参数一:用 obj 对象调用该方法;参数二:调用方法的传递的参数;返回值:方法的返回值(没有就不写) |
int getModifiers() |
获取修饰符 |
String getName() |
获取方法名称 |
int getParameterCount() |
获取参数个数 |
Parameter[] getParameters() |
获取全部参数 |
Class<?>[] getExceptionTypes() |
获取方法抛出的异常 |
setAccessible(boolean flag) |
设置为true,表示取消访问检查 |
import java.lang.reflect.*;
public class Main {
public static void main(String[] args) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
//获取class字节码文件对象
Class clazz = Class.forName("Student");
//获取公共成员方法(此时会包含父类的公共方法)
Method[] methods = clazz.getMethods();
for(Method method : methods) {
// System.out.println(method);
}
//获取私有的成员方法
Method[] methods1 = clazz.getDeclaredMethods();
for(Method method : methods1) {
System.out.println(method);
}
//public void Student.sleep()
//public void Student.setName(java.lang.String)
//private void Student.eat(java.lang.String)
//public void Student.setAge(int)
//public int Student.getAge()
//获取单个方法,需要传如方法的名字和形参
Method sleep = clazz.getDeclaredMethod("sleep"); //没有形参则不写形参
System.out.println(sleep); //public void Student.sleep()
Method eat = clazz.getDeclaredMethod("eat", String.class);
System.out.println(eat); //private void Student.eat(java.lang.String)
//获取方法修饰符
int modifiers = eat.getModifiers();
System.out.println(modifiers); //2
//获取方法名字
String name = eat.getName();
System.out.println(name); //eat
//获取方法形参
Parameter[] parameters = eat.getParameters();
for(Parameter parameter : parameters) {
System.out.println(parameter);
}
//java.lang.String arg0
//获取方法抛出的异常
Class<?>[] exceptionTypes = eat.getExceptionTypes();
for (Class<?> exceptionType : exceptionTypes) {
System.out.println(exceptionType);
}
//方法运行
eat.setAccessible(true); //屏蔽private的权限
Student student = new Student();
eat.invoke(student, "汉堡包"); //student表示方法调用者,"汉堡包"表示方法调用时传递的实际参数
//在吃汉堡包
}
}
反射的实际运用
import java.lang.reflect.*;
public class Main {
public static void main(String[] args) throws IllegalAccessException {
//对于任意一个对象,获取所有的字段名和值,并打印
Student student = new Student("张三", 18, '男');
printObject(student);
//name = 张三
//age = 18
//gender = 男
}
public static void printObject(Object obj) throws IllegalAccessException {
//获取字节码文件对象
Class clazz = obj.getClass();
//获取所有成员变量
Field[] fields = clazz.getDeclaredFields();
for(Field field : fields) {
//屏蔽private的权限影响
field.setAccessible(true);
//获取成员变量的名字
String name = field.getName();
//获取成员变量值
Object value = field.get(obj);
System.out.println(name + " = " + value);
}
}
}
多线程&JUC
基础概念
进程:进程是程序的基本执行实体。(一个程序运行就可以看作一个进程)
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。(一个程序运行可以看作一个进程,这个程序当中互相独立又可以同时运行的功能可以叫做线程)
单线程程序:
int a = 10; //等待0.01s
int b = 20; //等待0.01s
int c = a + b; //等待0.01s
多线程一般用于软件中的耗时操作:
- 拷贝、迁移大文件。
- 加载大量的资源文件。
多线程可以提高工作效率,只要想让多个事情同时运行就需要用到多线程。
并发与并行
并发:同一时刻,有多个指令在单个 CPU 上交替执行。
并行:同一时刻,有多个指令在多个 CPU 上同时执行。
2核4线程:可以同时并行 4 线程。如果超过 4 个线程,则会在其中随意切换。
多线程的三种实现方式
- 继承 Thread 类的方式进行实现。(线程与任务绑定死,无返回值)
- 实现 Runnable 接口的方式进行实现。(线程与任务分离开,无返回值)
- 利用 Callable 接口和 Future 接口方式实现。(线程与任务分开,有返回值)
继承Thread类的方式进行实现:
public class MyThread extends Thread {
//重写run方法
@Override
public void run() {
for(int i = 0; i < 100; ++i) {
System.out.println(getName() + "hello world");
//Thread类封装了getName方法,可以获取线程名字
}
}
}
public class Main {
public static void main(String[] args) {
/*多线程的第一种启动方式
* 1. 自定义一个类继承Thread类
* 2. 重写run方法
* 3. 创建子类对象并利用start启动线程
* */
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//为了区分线程,我们给线程取个名字
t1.setName("线程1");
t2.setName("线程2");
t1.start(); //启动线程
t2.start(); //启动第二个线程
//最终可以看到的是两个线程交替执行,是并发执行
}
}
实现 Runnable 接口的方式进行实现:
public class MyRun implements Runnable {
//重写run方法
@Override
public void run() {
for(int i = 0; i < 100; ++i) {
//利用静态方法currentThread获取当前线程的对象
Thread t = Thread.currentThread();
System.out.println(t.getName() + "hello world");
}
}
}
public class Main {
public static void main(String[] args) {
/*多线程的第二种启动方式
* 1. 自定义一个类实现Runnable接口
* 2. 重写run方法
* 3. 创建类对象,再创建Thread类对象,利用start启动线程
* */
//创建MyRun对象,表示要执行的任务
MyRun mr = new MyRun();
//将任务传递给线程,两个线程执行的任务相同
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//给线程设置名字
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
//结果也是两个线程交替进行,是并发执行
}
}
利用 Callable 接口和 Future 接口方式实现:
import java.util.concurrent.Callable;
public class MyCallable implements Callable {
//重写call方法
// @Override
// public Object call() throws Exception {
// return null;
// }
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i <= 100; ++i) {
sum += i;
}
return sum;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*多线程的第三种启动方式
* 特点:可以获取到多线程运行的结果,即返回值
* 1. 自定义类实现Callable接口
* 2. 重写call方法,该方法是有返回值的,代表运行的结果
* 3. 创建自定义类对象,表示要执行的任务
* 4. 创建FutureTask对象,用于管理多线程运行的结果
* 5. 创建Thread类对象,表示线程,最后启动
* */
//创建MyCallable对象,表示要执行的任务
MyCallable mc = new MyCallable();
//创建FutureTask对象,管理多线程运行的结果
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
t1.start();
//获取运行结果
Integer result = ft.get();
System.out.println(result); //5050
}
}
多线程常见的成员方法
方法名称 | 说明 |
---|---|
String getName() |
返回线程名称 |
void setName(String name) |
设置线程名字 |
static Thread currentThread() |
获取当前线程对象 |
static void sleep(long time) |
线程休眠,单位为毫秒 |
setPriority(int newPriority) |
设置线程优先级 |
final int getPriority() |
获取线程优先级 |
final void setDaemon(boolean on) |
设置为守护线程 |
public static void yield() |
出让线程、礼让线程 |
public static void join() |
插入线程、插队线程 |
class MyThread extends Thread {
public MyThread() {
super();
}
//重写构造方法
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for(int i = 0; i < 10; ++i) {
System.out.println(getName() + ":" + i);
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
// MyThread myThread = new MyThread();
// myThread.start(); //线程默认名字:Thread-x x代表从0开始的索引
// MyThread t1 = new MyThread("线程1");
// t1.start();
Thread thread = Thread.currentThread();
System.out.println(thread.getName()); //main方法的线程叫做main
System.out.println("---");
Thread.sleep(5000); //停留5s
System.out.println("---");
}
}
线程的优先级
线程的调度分为抢占式调度和非抢占式调度。
- 抢占式调度:所有线程抢占般地执行,随机性很强,执行哪个,执行多长时间都不确定。
- 非抢占式调度:所有线程轮流执行,执行时间也是差不多的。
Java 当中,使用的是抢占式调度。优先级最大为 10,最小为 1,默认为 5。
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10; ++i) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class Main {
public static void main(String[] args) {
//创建任务
MyRunnable myRunnable = new MyRunnable();
//分配任务
Thread t1 = new Thread(myRunnable, "线程1");
Thread t2 = new Thread(myRunnable, "线程2");
//查看优先级
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
//main的优先级
System.out.println(Thread.currentThread().getPriority());
//以上输出均为5
//设置优先级
t1.setPriority(1);
t2.setPriority(10);
//此时t2抢到cpu的概率更大,t2先完成的概率更大
t1.start();
t2.start();
}
}
守护线程
当其他的非守护线程执行完毕后,守护线程才会陆续结束。守护线程不一定全部执行完成,也不一定是马上结束的。
实际应用:聊天的时候传输文件,当聊天框关闭的时候,传输文件的线程也要陆续关闭。这个时候可以将传输文件的线程设置为守护线程。
class MyThread1 extends Thread {
@Override
public void run() {
for(int i = 0; i < 5; ++i) {
System.out.println(getName() + "@" + i);
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; ++i) {
System.out.println(getName() + "@" + i);
}
}
}
public class Main {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("聊天");
t2.setName("传输文件");
//把第二个线程设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start(); //t2最终并没有执行到99
}
}
礼让线程
该线程会主动出让 CPU 的使用权,比较少用。
class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; ++i) {
System.out.println(getName() + "@" + i);
//表示出让当前CPU的执行权
Thread.yield();
//上述代码会使得t1与t2的运行尽量均匀
}
}
}
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("飞机");
t2.setName("坦克");
t1.start();
t2.start();
}
}
插入线程
可以将该线程插入到当前线程之前执行。
class MyThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; ++i) {
System.out.println(getName() + "@" + i);
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.setName("土豆");
t.start();
t.join(); //把t的线程插入到当前线程之前
for (int i = 0; i < 10; i++) {
System.out.println("main线程" + i);
}
}
}
线程的生命周期
线程的六大状态:新建、就绪、死亡、阻塞、等待、计时等待。
创建线程对象 -> 就绪状态(在抢占 CPU 资源,有执行资格,但没有执行权) -> 运行状态(抢占到 CPU 了,有执行资格,也有执行权,为了方便理解,我们加上了运行状态,但实际 Java 的规定中并没有这个运行状态)-> 执行完毕(线程死亡,变成垃圾)
当线程运行时,也有可能被其他线程抢去 CPU 的使用资格,这个时候该线程回到就绪状态。
当线程运行时,还有可能碰到 sleep
方法或其他阻塞式的方法使得线程阻塞,这个时候线程没有执行资格,也没有执行权。之后又回到就绪状态。也就是说,sleep
方法使得线程睡眠后,该线程是不会立马执行的,因为这个时候线程会回到就绪状态。
当线程运行时,如果遇到 wait
方法则会进入等待状态,直到利用 notify
方法唤醒,唤醒之后,线程进入就绪状态。
// 没有执行资格,没有执行权
// 阻塞、(计时)等待
// +--------<--------+ sleep或者其他阻塞方法
// | |
// +--------<--------+ 被其他线程抢走CPU执行权
// | |
//创建线程对象 -> 有执行资格,没有执行权 -> 有执行资格,有执行权 -> 线程死亡,变成垃圾
// 新建 就绪 *运行 死亡
同步代码块
利用Thread.sleep(1000)
让线程睡1s,模拟线程频繁抢占 CPU 资源的情况。这种时候会让读写发生冲突。使得最终结果出错。操作共享数据时影响更明显。
这个时候,我们需要利用 synchronized
关键字上锁,使得线程进行的时候不会受其他线程的影响。锁是默认打开的,当有一个线程进去了,锁就会自动关闭。当里面的代码全部执行完毕,线程才会出来,锁自动打开。
需要保证 synchronized
的锁对象唯一,因为如果不唯一,那么代表这个代码块有多把锁,失去了唯一性,没有办法确保数据安全。
class MyThread extends Thread {
static int ticket = 0;
//锁对象需要保证唯一性,加上static
//static Object obj = new Object();
@Override
public void run() {
while(true) {
synchronized (MyThread.class) { //传入锁对象,一般传入当前类的字节码文件
//利用synchronized关键字对下方的代码上锁
//-------------------------------------------------//
if(ticket < 100) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
++ticket;
System.out.println(getName() + "正在卖第" + ticket + "张票");
}
else break;
//-------------------------------------------------//
}
}
}
}
同步方法
格式:修饰符 synchronized 返回值类型 方法名(参数列表) {...}
特点:
- 同步方法是锁住方法里面的所有代码。
- 锁对象不能自己定。是 Java 已经规定好的。如果当前方法是非静态的,则锁对象是 this,即当前方法的调用者。如果当前方法是静态的,则锁对象是当前类的字节码文件。
书写的时候,先写同步代码块,然后再改写成同步方法。idea 中利用 Ctrl + Alt + M 可以快速提取方法。
class MyRunnable implements Runnable {
private int ticket = 0; //MyThread作为线程任务,只创建一个,故这里可以不用static
@Override
public void run() {
while(true) {
if (method()) break;
}
}
private synchronized boolean method() {
if(ticket >= 100) {
return true;
}
else {
++ticket;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}
Lock 锁
使用同步代码块或者同步方法的时候,锁是自动加且自动释放的。程序员无法去进行控制。为了更清晰的表达如何加锁和释放锁,JDK5 之后提供了一个新的对象 Lock。
Lock 实现了更加广泛的锁定操作。利用lock()
方法获得锁,利用unlock()
释放锁。
要注意的是,Lock 是接口,不能直接实例化,所以我们需要采用其实现类 ReentrantLock 来实例化。
class MyThread extends Thread {
static int ticket = 0;
static Lock lock = new ReentrantLock(); //使用ReentrantLock创建lock的实现类对象
//加上static表示所有对象共用一把锁
@Override
public void run() {
while(true) {
// synchronized (MyThread.class) {
// 加锁
try {
lock.lock();
if(ticket == 10) {
break;
}
else {
Thread.sleep(1000);
++ticket;
System.out.println(getName() + "在卖第" + ticket + "张票");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock(); //利用finally一定会执行的特性,将解锁语句放入其中,确保锁一定会被执行
}
}
//}
}
}
死锁
锁的嵌套,即锁外还嵌套锁,是一个错误。
死锁场景可以模拟成如下形式:一张桌子上有一双筷子,A 与 B 在这张桌子上吃饭。A 与 B 一次性只能拿一只筷子,当其中一个人拿到一双筷子的时候才能够开始吃饭。一种情况如下:A 抢到了左边的筷子,B 抢到了右边的筷子。双方都在等待对方把筷子放下,这个时候,出现卡死情况。
更详细的版本请看:哲学家就餐问题。
class MyThread extends Thread {
static Object object1 = new Object();
static Object object2 = new Object();
@Override
public void run() {
while(true) {
if("线程A".equals(getName())) {
synchronized (object1) {
System.out.println("线程A拿到了A锁,准备拿B锁"); //这个时候object1关闭
synchronized (object2) {
System.out.println("线程A拿到了B锁,顺利执行完一轮");
}
}
}
else if("线程B".equals(getName())) {
synchronized (object2) {
//A虽然先抢到object1,但是此时的判断是object2
//这个时候不影响object2的执行,所以B进来,把object2关上
System.out.println("线程B拿到了B锁,准备拿A锁");
synchronized (object1) {
System.out.println("线程B拿到了A锁,顺利执行完一轮");
}
}
}
//两个锁都已经锁上了,导致线程卡死,进行不下去
}
}
}
线程栈
每一个线程开启之后都会有一个属于自己的栈空间。堆区中维护的是成员变量,run
方法的运行是在栈空间当中运行的。同理,run
方法创建的成员变量也会放入线程栈当中。
生产者和消费者模式
生产者和消费者模式又称为等待唤醒机制。是一个十分经典的多线程协作的模式。
该模式可以让两个线程交替执行。其中一条线程生产数据,称为生产者;另一条线程消费数据,称为消费者。但是一开始线程是随机执行的,所以需要有一个物件来控制两个线程的执行过程,即利用等待和唤醒对线程的执行进行控制。
消费者逻辑:
- 判断是否有数据。
- 如果没有则等待。
- 如果有则进行消费。
- 消费完毕,唤醒生产者生产数据。
生产者逻辑:
- 判断是否有数据。
- 有则等待。
- 没有则生产数据,并唤醒消费者。
方法 | 说明 |
---|---|
void wait() |
当前线程等待,直到被唤醒 |
void notify() |
随机唤醒单个线程 |
void notifyAll() |
唤醒所有线程 |
class Cook extends Thread {
@Override
public void run() {
while(true) {
synchronized (Desk.lock) {
//判断共享数据是否使用完毕
if(Desk.count == 0) {
break;
}
else { //共享数据没有到末尾,则执行核心逻辑
//如果有数据,则等待,等待消费者消费数据
if(Desk.foodFlag == 1) {
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
else { //如果没有数据,则开始生产数据
System.out.println("生产者生产了数据");
Desk.foodFlag = 1;
//唤醒等待的消费者
Desk.lock.notifyAll();
}
}
}
}
}
}
class Foodie extends Thread {
@Override
public void run() {
while(true) {
synchronized (Desk.lock) {
if(Desk.count == 0) { //共享数据使用完毕,停止循环
break;
}
else { //共享数据没有使用完毕,执行核心逻辑
//先判断有没有数据,如果没有,则等待
if(Desk.foodFlag == 0) {
try {
Desk.lock.wait(); //使用锁对象调用wait方法
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 让当前线程和锁进行绑定,唤醒的时候可以唤醒这把锁绑定的所有线程
}
//如果有,则开始消费
else {
Desk.count--;
System.out.println("消费者消费数据,还能再消费" + Desk.count + "次");
//然后唤醒生产者继续生产
Desk.lock.notifyAll();
Desk.foodFlag = 0;
}
}
}
}
}
}
class Desk {//利用桌子来控制线程的执行
public static int foodFlag = 0; //桌子上是否有面条,foodFlag = 0表示没有面条
public static int count = 10; //最多使用次数
public static Object lock = new Object(); //锁对象
}
public class Main {
public static void main(String[] args) {
Cook c = new Cook();
Foodie f = new Foodie();
c.setName("生产者");
f.setName("消费者");
c.start();
f.start();
}
}
阻塞队列实现等待唤醒机制
阻塞队列(BlockingQueue),其实现了 Iterable、Collection、Queue、BlockingQueue
四个接口,其下有两个实现类,分别是 ArrayBlockingQueue
(数组实现,有界)和 LinkedBlockingQueue
(链表实现,最大容量是 int 的最大值)。
阻塞队列实现等待唤醒机制的时候,生产者和消费者必须使用同一个队列。使用阻塞队列的时候,不需要自己额外加锁,阻塞队列内部已经写好了一个锁。
import java.lang.reflect.Array;
import java.util.concurrent.ArrayBlockingQueue;
class Cook extends Thread {
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true) {
//将数据塞入队列
try {
queue.put("数据");
System.out.println("生产者生产数据");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Foodie extends Thread {
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true) {
try {
String food = queue.take();
System.out.println("消费者消费数据");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Main {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
c.setName("生产者");
f.setName("消费者");
c.start();
f.start();
}
}
多线程的综合练习
抢红包
微信中的抢红包用到了多线程。假设有一个100块的红包,分成3个包,现有5个人去抢。5个人是5条线程,没抢到的需要打印没抢到,抢得到的需要告知抢到了多少钱。
注意:抢红包时,需要设置一个最小的获奖金额,以防出现抢到红包,但是金额为0的情况。并且,每一次随机的时候也要注意控制范围,不能存在一次性把所有金额全抢走的情况。
import java.text.DecimalFormat;
import java.util.Random;
public class MyThread extends Thread {
//共享数据
static private double money = 100; //总金额
static private int count = 3; //红包个数
static private final double MIN = 0.01; //最小的中奖金额
private DecimalFormat df = new DecimalFormat("#0.00"); //设置输出格式
@Override
public void run() {
synchronized (MyThread.class) {
if(count == 0) {
System.out.println(getName() + "没抢到");
}
else {
//前两次随机,最后一次直接拿走全部金额
if(count == 1) {
--count;
System.out.println(getName() + "抢到了" + df.format(money));
}
else {
--count; //红包个数减1
Random random = new Random();
//注意控制范围,既要保证能抽到,又要保证不能一次性全抽走
double m = Math.max(MIN, random.nextDouble(money -(count - 1) * MIN));
money -= m;
System.out.println(getName() + "抢到了" + df.format(m));
}
}
}
}
}
线程池
以前使用线程的时候,需要用到就创建,用完之后线程就销毁。这样子会比较浪费操作系统的资源。我们可以去准备一个容器去存放线程,这个容器就叫做“线程池”。当创建一个任务时,线程池会创建一个线程,但是完成任务之后不会销毁该线程,下一次需要跑任务的时候,这个线程就可以用上。如果提交任务时,线程池中没有空闲的线程,这个时候任务就会排队。
线程池实现:
- 创建线程池。
- 提交任务。
- 所有的任务全部执行完毕,关闭线程池(实际开发中线程池是不会关闭的,是24小时开启的,保证可以随时随地登录使用)。
可以使用线程池工具类Executors
调用方法返回不同类型的线程对象。
方法名称 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() |
创建一个上限为int的最大值的线程池 |
public static ExecutorService newFixedThreadPool(int nThreads) |
创建有上限的线程池 |
public Future<?> submit(Runnable/Callable) |
向线程池当中提交任务 |
public void shutdown() |
销毁线程池 |
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
//获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
//给线程池对象提交任务
pool1.submit(new MyRunnable());
//销毁线程池
pool1.shutdown();
}
}
自定义线程池
线程池工具类中,调用newCachedThreadPool
方法实际上在内部会去创建线程池类ThreadPoolExecutor
。为了使得线程池的创建更加灵活,我们可以自己去创建线程池类。
线程池的创建我们可以这么理解:假设现在有一家餐厅,实行一对一服务制度,即每来一名顾客,我们就让一个服务员去进行服务。为了提高效率,我们需要有正式员工和临时员工。正式员工一直在餐厅工作,当来的顾客太多的时候,我们就招聘临时员工,当临时员工空闲一定时间后,再将其解雇。考虑到顾客量极大的时候,餐厅只能服务一定量的顾客,这个时候导致一部分顾客在餐厅外等待。为防止餐厅崩溃的情况出现,我们需要限制在餐厅外等待的顾客。
核心元素:
- 正式员工的数量。
- 餐厅最大员工数。
- 临时员工空闲多长时间被辞退(值)。
- 临时员工空闲多长时间被辞退(单位)。
- 排队的顾客队列。
- 从哪里招人。
- 当排队人数过多,超出的顾客要拒绝服务。
上述7个核心元素对应线程池类的7个构造参数:
- 核心线程数量。
- 线程池中最大线程的数量。(大于等于核心线程数量)
- 空闲时间(值)。
- 空闲时间(单位)。(用
TimeUnit
指定) - 阻塞队列。(任务会优先交给核心线程处理,剩余的放入阻塞队列中,当阻塞队列放满且还有任务需要执行时,才会开启临时线程,这个时候还有任务剩余的话,则拒绝访问。线程的执行顺序和提交顺序不一定一致。)
- 创建线程的方式。(通过
Executors.defaultThreadFactory
创建。) - 要执行的任务过多时的解决方案。
执行任务过多时的解决方案有:
任务拒绝策略 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy |
默认策略:丢弃任务并抛出RejectedExecution 异常 |
ThreadPoolExecutor.DiscardPolicy |
丢弃任务,但是不抛出异常,这个是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy |
抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunPolicy |
调用任务的run 方法绕过线程池直接执行 |
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
//创建自己的线程池对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数量
6, //临时线程数量+核心线程数量(最大线程数量)
60, //空闲线程最大存活时间(60s)
TimeUnit.SECONDS, //指定时间单位
new ArrayBlockingQueue<>(3), //阻塞队列,这里指定长度为3
Executors.defaultThreadFactory(), //创建线程工厂
new ThreadPoolExecutor.AbortPolicy() //任务的拒绝策略
);
//利用submit可以向pool提交任务
//pool.submit()...
}
}
线程池多大合适
4核8线程:“4核”指该CPU最多可以并行做4件事情。“8线程”指利用超线程技术,将原本的4核虚拟成8个,此时最大并行数为8。
项目一般分为两种:一种是CPU密集型、另一种是IO密集型。如果是计算比较多,读取文件比较少,则是CPU密集型。反之则是IO密集型。现在的项目大部分是IO密集型,执行IO操作的时候,CPU是空闲状态。
对于CPU密集型:
$$
线程池的大小 = 最大并行数 +1
$$
对于IO密集型:
$$
线程池的大小=最大并行数\times期望CPU利用率\times\frac{CPU计算时间+等待时间}{CPU计算时间}
$$
CPU的计算、等待时间可以利用thread dump
工具进行测试。
多线程的额外扩展内容
扩展内容开发中比较少用,但是面试中比较常见。涉及到:volatile、JMM、悲观锁、乐观锁、CAS、原子性、并发工具类(ConcurrentHashMap、CountDownLatch、CyclicBarrier、Semaphore、Exchanger)等,敬请期待。
网络编程
网络编程就是在网络通信协议下,不同计算机上运行的程序,进行的数据传输。应用场景:即时通信、网游对战、金融证券、国际贸易、邮件等。涉及到计算机与计算机之间的传输。Java中可以使用java.net
包下的技术轻松开发出常见的网络应用程序。
常见的软件架构有两种:CS、BS。不管是哪种,真正的核心处理逻辑都是在服务器上:
CS:Client/Server(客户端 / 服务器),采取这种架构的软件,在用户本地需要下载并安装客户端程序,在远程有一个服务器端程序。比如:QQ、steam。
优点:事先下载好所有资源,用户体验好。缺点:需要开发客户端和服务端,开发、部署、维护麻烦,服务端更新时,客户端也需要更新。CS架构适合定制专业化的办公类软件。
BS:Brower/Server(浏览器 / 服务器),采取这种架构时,只需要一个浏览器,用户通过不同的网址就可以访问不同的服务器。比如:京东、淘宝(网页端)。
优点:方便,不需要开发客户端,只需要页面+服务端,且只需要打开浏览器就可以使用。缺点:需要通过网络传输所有的图片、音频资源,如果资源过大,则会降低用户体验。BS架构适合移动互联网应用。
网络编程三要素
当我们要向另外一台计算机发送信息时,我们要知晓对方电脑在互联网上的地址(IP),还需要确定对方电脑接受数据的软件(端口号,一个端口号只能被一个软件绑定使用),还需要确定网络传输的规则(协议)。故IP、端口号、协议就是网络编程三要素。
网络编程三要素:
- IP:设备在网络中的地址,是唯一的标识。
- 端口号:应用程序在设备中唯一的标识。
- 协议:数据在网络中传输的规则,常见的协议有UDP、TCP、http、https、ftp。
IP
全称:Internet Protocol,是互联网协议地址,也称IP地址。是分配给上网设备的数字标签。常见的IP分为:ipv4、ipv6。
IPv4:全称为Internet Protocol version 4,即互联网通信协议第四版。采用32位地址长度,分成4组(每组1字节,8位)。一般用点分十进制表示法,例如:192.168.1.66
。每一组最大值是255,最小值是0。在IPv4中,每一组有256种表示方法,一共4组,最多可以表示 $256^4 = 4294967296$ 种地址,数量有限,不够使用。实际上,在2019年11月26日,IPv4的全部可用地址就已经分配完毕了。
IPv6:全称为Internet Protocol version 6,即互联网通信协议第六版。由于互联网的蓬勃发展,IP地址的需求量越来越大,而IPv4模式下的IP总数有限,为了让地址能够继续分配,故出现了IPv6。采用128位地址长度,分成8组(每组2字节,16位)。一共有 $2^{128} = 3.04\times10^{38}$ 种地址,这个数量,可以给地球上的每一粒沙子都编上号。一般用冒分十六进制表示法,例如:2001:0DB8:0000:0023:0008:0800:200C:417A
。对于每一组,可以省略前导0,此时,对于上述IP地址,可以记录为:2001:DB8:0:23:8:800:200C:417A
。此外,还有一种特殊情况:0位压缩表示法。即如果计算出的16进制表示形式中有多个连续的0,可以利用::
进行压缩表示,例如对于FF01:0:0:0:0:0:0:1101
,我们可以压缩为:FF01::1101
。
上述是对于IPv4和IPv6的基本介绍,现对IPv4进行额外补充。
IPv4的地址分类形式有两种:一种是公网地址(万维网使用),一种是私有地址(局域网使用)。192.168.
开头的就是私有地址,范围即为192.168.0.0--192.168.255.255
,专门为组织机构内部使用,通过共享公网IP以此节省IP。
一个特殊的IP地址为:127.0.0.1
,又称localhost。是回送地址,也叫本地回环地址,又称本机IP。永远只会寻找当前所在的本机。换个地方上网,局域网IP可能不一样,因为这个是通过路由器分配的。通过localhost,可以保证自己给自己发送数据时不出错。
在 Java 中,InetAddress 类表示互联网协议地址(IP)。其有两个子类,Inet4Address 和 Inet6Address,对应了IPv4和IPv6。
import java.net.InetAddress;
import java.net.UnknownHostException;
public class Main {
public static void main(String[] args) throws UnknownHostException {
//获取InetAddress对象,通过静态方法获取,传入ipv4地址
InetAddress address = InetAddress.getByName("192.168.10.140");
System.out.println(address); // /192.168.10.140
//获取电脑主机名
String name = address.getHostName();
System.out.println(name); //LAPTOP-99D5P3O4.lan
//获取InetAddress对象,通过静态方法获取,传入计算机名称
InetAddress address1 = InetAddress.getByName("LAPTOP-99D5P3O4");
System.out.println(address1); // LAPTOP-99D5P3O4/192.168.10.140
//获取ip地址
String hostAddress = address1.getHostAddress();
System.out.println(hostAddress); //192.168.10.140
}
}
端口号
应用程序在设备中唯一的标识。一个端口号只能被一个应用程序使用。
端口号是由两个字节表示的整数,取值范围:0-65535。其中0-1023之间的端口号是用于一些知名的网络服务或者应用。我们自己使用1024以上的端口号就可以了。
协议
计算机网络中,连接和通信的规则被称作网络通信协议。
OSI参考模型:世界互联协议标准,全球通信规范,单模型过于理想化,未能在因特网上进行广泛推广。
TCP / IP 参考模型:事实上的国际标准。
UDP协议
用户数据报协议(User Datagram Protocol),是面向无连接(不管两台计算机是否建立连接,直接传输数据)通信协议。速度快,有大小限制,一次最多发送64K,数据不安全,易丢失数据。应用场景:网络会议、语音通话、在线视频。(丢失数据的影响不大)
UDP通信程序(发送数据):
过程类似于寄快递。首先找快递公司,然后打包快递,接着快递公司发送包裹,最后我们付钱走人。
易得发送数据有如下步骤:
- 创建发送端的DatagramSocket对象。
- 数据打包(DatagramPacket)。
- 发送数据。
- 释放资源。
import java.io.IOException;
import java.net.*;
public class Main {
public static void main(String[] args) throws IOException {
//发送数据
//创建DatagramSocket对象
//绑定端口,以后我们就是通过这个端口往外发送
//空参:在所有可用的端口中随机一个进行使用 空参:指定端口号进行绑定
DatagramSocket ds = new DatagramSocket();
//打包数据
String str = "你好你好。";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10086; //往哪个端口发送
//参数如下:数据内容,要发哪些数据,发送至哪个地址,发送至哪个端口
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
//发送数据
ds.send(dp);
//释放资源
ds.close();
}
}
UDP通信程序(接收数据):
过程类似于取快递。首先找快递公司,然后接受快递包,接着从包当中取出东西,最后签收走人。
易得接受数据有如下步骤:
- 创建接受端的 DatagramSocket 对象。
- 接收打包好的数据。
- 解析数据包。
- 释放资源。
import java.io.IOException;
import java.net.*;
public class Main {
public static void main(String[] args) throws IOException {
//接受数据
//创建DatagramSocket对象
//在接受的时候,一定要绑定端口,而且要和发送的端口保持一致
DatagramSocket ds = new DatagramSocket(10086);
//接受数据包
byte[] bytes = new byte[1024];
//参数如下:接受到哪里,接受多长
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//receive方法是阻塞的,程序运行到这里会死等,直到发送端发送出消息
ds.receive(dp);
//解析数据
byte[] data = dp.getData(); //获取到的数据
int length = dp.getLength(); //获取到多少个字节数据
InetAddress address = dp.getAddress(); //从哪台电脑发过来的
int port = dp.getPort(); //对方从哪个端口发过来的
System.out.println("接受到的数据:" + new String(data, 0, length));
System.out.println("该数据是从" + address + "这台电脑中的" + port + "端口发出的");
//释放资源
ds.close();
}
}
UDP 的三种通信方式:单播,组播,广播。
- 单播:一台计算机给另一台计算机发送数据。(上面的UDP通信程序代码就是单播)
- 组播:一台计算机给一组计算机发送数据。(组播地址:
224.0.0.0 -- 239.255.255.255
,其中224.0.0.0 -- 224.0.0.255
为预留的组播地址) - 广播:一台计算机给局域网中所有计算机发送数据。(广播地址:
255.255.255.255
)
利用 MulticastSocket
来进行组播和广播,以下演示组播:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class Send {
public static void main(String[] args) throws IOException {
//发送数据
//创建MulticastSocket对象进行组播
MulticastSocket ms = new MulticastSocket();
//打包数据
String s = "你好你好。";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("224.0.0.1"); //指定组播地址
//如果要广播,上述地址指定为255.255.255.255
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
//发送数据
ms.send(dp);
//释放资源
ms.close();
}
}
import java.io.IOException;
import java.net.*;
public class Main {
public static void main(String[] args) throws IOException {
//接收数据
//创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket(10000);
//将当前本机加入到224.0.0.1这一组中
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address);
//创建数据包对象
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//接受数据
ms.receive(dp);
//解析数据
byte[] data = dp.getData();
int length = dp.getLength();
String ip = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();
System.out.println("ip为:" + ip + "的人,发送了数据:" + new String(data, 0, length));
//释放资源
ms.close();
}
}
TCP协议
传输控制协议(Transmission Control Protocol),是面向连接的通信协议。速度慢,没有大小限制,数据安全。应用场景:下载软件、文字聊天、发送邮件。(丢失数据影响较大)
这是一种可靠的网络协议,它在通信两端各建立一个 Socket
对象,通信之前要保证连接已经建立,通过 Socket 产生 IO 流来进行网络通信。
对于客户端(发送数据):
- 创建 Soket 对象与指定服务器连接。
- 获取输出流,写数据。
- 释放资源。
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Send {
public static void main(String[] args) throws IOException {
//创建Socket对象,在创建对象的同时会连接服务端
Socket socket = new Socket("127.0.0.1", 10086);
//可以从连接通道中获取输出流
OutputStream os = socket.getOutputStream();
//写出数据
os.write("你好你好".getBytes());
//释放资源
os.close();
socket.close();
}
}
对于服务器(接受数据):
- 创建 ServerSocket 对象。
- 监听客户端连接,返回一个 Socket 对象。
- 获取输入流,读数据,并把数据显示在控制台。
- 释放资源。
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
public class Main {
public static void main(String[] args) throws IOException {
//接受数据
//创建ServerSocket
ServerSocket ss = new ServerSocket(10086);
//监听客户端连接,死等客户端来连,返回客户端的连接对象
Socket accept = ss.accept();
//获取输入流读取数据
InputStreamReader is = new InputStreamReader(accept.getInputStream());
int read;
while((read = is.read()) != -1) {
System.out.println((char)read);
}
//释放资源
accept.close(); //断开连接
ss.close(); //关闭服务器
}
}
三次握手与四次挥手:
三次握手发生在客户端连接服务器时,是为了保证连接的建立。
- 客户端向服务器发出连接请求,等待服务器确认。
- 服务器向客户端返回一个响应,告诉客户端收到了请求。
- 最后客户端发出确认信息,与服务器建立连接。
四次挥手发生在客户端发送资源给服务器时,是为了确保数据处理完毕后才断开连接。
- 客户端向服务器发出取消连接请求。
- 服务器向客户端返回一个响应,表示收到客户端取消请求。
- 当服务器将最后的数据处理完毕后,再向客户端返回一个响应确认取消信息。
- 客户端收到取消信息响应后,发送确认信息,断开连接。
网络编程的综合练习
多发多收
使用 TCP 协议,客户端多次发送数据,服务端多次接受数据,并打印。
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
//客户端,发送数据,确定发到哪里,用哪个端口发
Socket socket = new Socket("127.0.0.1", 10086);
//录入数据并写出
Scanner sc = new Scanner(System.in);
OutputStreamWriter osw = new OutputStreamWriter(socket.getOutputStream());
//写出
while(true) {
String text = sc.nextLine();
osw.write(text);
osw.flush(); //如果使用转换流传输数据,需要用flush刷新缓冲区,否则数据无法传输出去
if("886".equals(text)) break;
}
//释放资源
osw.close();
socket.close();
}
}
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
//服务端,接受数据,确定用哪个端口接受
ServerSocket ss = new ServerSocket(10086);
//等待客户端的连接
Socket socket = ss.accept();
//读取数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
char[] chars = new char[1024];
int len = 0;
while((len = isr.read(chars)) != -1) {
System.out.println(new String(chars, 0 ,len));
}
//因为客户端一直处于循环状态,没有向服务端发送断开连接请求
//四次挥手无法执行,所以二者不会断开连接
//故服务端不需要写循环
//释放资源
isr.close();
socket.close();
ss.close();
}
}
接受和反馈
客户端发送一条数据,接收服务端反馈的消息并打印。服务端接收数据并打印,再给客户端反馈消息。
提示:当 soket 对象开启连接之后,我们只需要通过 IO 流在两个端口之间进行传输数据就可以了。
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
//客户端,发送数据
Socket socket = new Socket("127.0.0.1", 10086);
//发送数据
String str = "见到你很高兴";
OutputStream os = socket.getOutputStream();
os.write(str.getBytes());
//发送完数据及时写出结束标记
socket.shutdownOutput();
//接收数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
int read;
while((read = isr.read()) != -1) {
System.out.print((char)read);
}
//释放资源
socket.close();
}
}
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
//服务端,接收数据
ServerSocket ss = new ServerSocket(10086);
//等待客户端连接
Socket socket = ss.accept();
//接收数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
int read;
while((read = isr.read()) != -1) {
System.out.print((char)read);
}
//回写数据
String str = "到底有多开心呢";
OutputStream os = socket.getOutputStream();
os.write(str.getBytes());
//释放资源
socket.close();
ss.close();
}
}
上传文件
客户端将本地文件上传到服务器,接收服务器的反馈。服务器接收客户端上传的文件,上传完毕之后给出反馈。
提示:利用 FileInputStream
读取本地文件到客户端程序,然后利用网络IO将文件在客户端和服务端中间进行数据传输,最后利用FileOutputStream
将文件写至服务端本地。利用UUID
类获取一个随机的文件名:UUID.randomUUID().toString().replace("-", "")
。
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
//创建Socket对象发送数据
Socket socket = new Socket("127.0.0.1", 10086);
//发送数据
OutputStream os = socket.getOutputStream();
//复制本地文件并发送
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
int read;
while((read = fis.read()) != -1) {
os.write(read);
}
fis.close();
//写出完毕
socket.shutdownOutput();
//接收服务器的反馈
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
while((read = isr.read()) != -1) {
System.out.print((char) read);
}
//释放资源
socket.close();
}
}
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
public class Server {
public static void main(String[] args) throws IOException {
//创建ServerSocket对象接收数据
ServerSocket ss = new ServerSocket(10086);
//与客户端产生连接
Socket socket = ss.accept();
//接收数据
//这里相当于拷贝文件,不需要特殊解码,所以使用InputStream即可
InputStream is = socket.getInputStream();
//复制文件信息到本地
String parent = "D:\\HNU";
String son = UUID.randomUUID().toString().replace("-", "") + ".txt";
File file = new File(parent, son);
FileOutputStream fos = new FileOutputStream(file);
int read;
while((read = is.read()) != -1) {
fos.write(read);
}
fos.close();
//反馈信息给客户端
String res = "上传成功";
OutputStream os = socket.getOutputStream();
os.write(res.getBytes());
//释放资源
socket.close();
ss.close();
}
}
上传文件(多线程版)
想要服务器不停止,能接收很多用户上传的文件。
提示:可以用循环或者多线程。但是循环不合理,最优解法是(循环 + 多线程)改写。
Client 类的代码不变。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
//创建ServerSocket对象接收数据
ServerSocket ss = new ServerSocket(10086);
while(true) {
//与客户端产生连接
Socket socket = ss.accept();
//连接成功后,开启一条线程
//一个用户就对应一条线程
new Thread(new MyRunnable(socket)).start();
}
}
}
import java.io.*;
import java.net.Socket;
import java.util.UUID;
public class MyRunnable implements Runnable {
private Socket socket;
public MyRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//接收数据
//...这段代码和上传文件那一节的Server类一致
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//释放资源
if(socket != null) { //注意进行非空判断
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
上传文件(线程池优化)
频繁创建线程并销毁非常浪费系统资源,所以需要用线程池优化。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Server {
public static void main(String[] args) throws IOException {
//创建线程池对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数量
16, //线程池总大小
60, //空闲时间(值)
TimeUnit.SECONDS, //空闲时间(单位)
new ArrayBlockingQueue<>(2), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
//创建ServerSocket对象接收数据
ServerSocket ss = new ServerSocket(10086);
while(true) {
//与客户端产生连接
Socket socket = ss.accept();
//连接成功后,开启一条线程
//一个用户就对应一条线程
MyRunnable myRunnable = new MyRunnable(socket);
pool.submit(myRunnable);
}
}
}
接收浏览器信息并打印(BS架构)
客户端:不需要写。服务端:接收数据并打印。
代码与 TCP 协议的 Server 类一致,只需要在浏览器的网址中输入127.0.0.1:端口号
即可。
动态代理
要给代码增加新的功能时,一种方法是直接去修改原来的代码,这种叫侵入式修改,有可能牵一发而动全身。而动态代理就是无侵入式的给代码增加额外的功能。
或者当对象干的事太多的话,也可以通过代理来转移部分职责。对象有什么方法想被代理,代理就一定要有对应的方法。我们可以将要代理的方法置入接口当中,利用接口进行动态代理。
java.lang.reflect.Proxy
类,提供了为对象产生代理对象的方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
- 参数一:用于指定用哪个类加载器,去加载生成的代理类。
- 参数二:指定接口,这些接口用于指定生成的代理有什么方法。
- 参数三:用来指定生成的代理对象要干什么事情。
public interface Star {
public abstract String sing(String name);
public abstract void dance();
}
public class BigStar implements Star {
private String name;
public BigStar() {
}
public BigStar(String name) {
this.name = name;
}
//唱歌 跳舞
@Override
public String sing(String name) {
System.out.println(this.name + "正在唱" + name);
return "谢谢";
}
@Override
public void dance() {
System.out.println(this.name + "正在跳舞");
}
//...
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/*
* 类的作用:创建一个代理
* */
public class ProxyUtil {
/*
* 方法的作用:给BigStar创建一个代理
* 形参:被代理的对象
* 返回值:创建的代理
* */
public static Star createProxy(BigStar bigStar) {
// 1. 参数一:用于指定用哪个类加载器,去加载生成的代理类。
// 2. 参数二:指定接口,这些接口用于指定生成的代理有什么方法。
// 3. 参数三:用来指定生成的代理对象要干什么事情。
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(), //类加载器
new Class[]{Star.class}, //要实现的接口Class数组
new InvocationHandler() { //指定生成的代理要做什么
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
* 参数一:代理的对象
* 参数二:要运行的方法
* 参数三:调用方法时传递的实参
* */
if("sing".equals(method.getName())) {
System.out.println("准备话筒");
}
else if("dance".equals(method.getName())) {
System.out.println("准备场地");
}
//代理的工作做完了,就要去调用对象的方法
//最后对方法的返回值进行返回即可
return method.invoke(bigStar, args);
}
}
);
return star;
}
}
public class Main {
public static void main(String[] args) {
//获取BigStar对象
BigStar bigStar = new BigStar("鸡哥");
//获取代理对象
Star proxy = ProxyUtil.createProxy(bigStar);
//调用唱歌的方法
String res = proxy.sing("只因你太美");
System.out.println(res);
//调用跳舞的方法
proxy.dance();
}
}
GUI
GUI(Graphical User Interface),又称图形用户接口,是指采用图形化的方式显示操作界面。几乎所有的语言都包含这类功能,Java 中有两个包具有这种功能:AWT
包和Swing
包,AWT 包版本较老,并且还可能会有兼容问题,所以我们一般使用 Swing 包。
Java 主要用于服务器后台开发,对于图形化界面要求不高,故此章节不作细致展示。
主界面一般分为:
- JFrame:最外层的窗体。
- JMenuBar:菜单栏。
- JLabel:管理文字和图片的容器。
上述被称为“组件”。
JFrame
import javax.swing.*;
public class Main {
public static void main(String[] args) {
//创建一个登录界面
JFrame loginJFrame = new JFrame();
//调用setSize方法设置宽高
loginJFrame.setSize(488, 430);
//调用setVisible方法将界面显示(默认隐藏),建议写在最后
loginJFrame.setVisible(true);
}
}
一般不会直接在 main
方法中直接书写窗体,而是将一个窗体抽取成一个类进行设计。注意要继承 JFrame
这个类。
import javax.swing.*;
public class LoginJFrame extends JFrame {
//表示登录主界面
public LoginJFrame() {
//在创建登录界面的时候,同时给这个界面设置一些信息,将这些代码放在构造方法中
this.setSize(488, 430);
//设置界面的标题
this.setTitle("xxx");
//设置界面置顶
this.setAlwaysOnTop(true);
//设置界面居中
this.setLocationRelativeTo(null);
//设置关闭模式
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
//取消默认的居中放置,只有取消了,才会按照xy轴形式添加组件
this.setLayout(null);
//将界面调出
this.setVisible(true);
}
}
//...
//接着在main方法中直接new对象即可
public class App {
public static void main(String[] args) {
//表示程序的启动入口
//如果要开启界面,则创建对应对象
new LoginJFrame();
}
}
JMenuBar
JMenuBar
是菜单栏,每一个栏位对应着一个JMenu
的类,每一个JMenu
类下拉框下面又对应着JMenuItem
对象。
JMenuItem
有一个 getText
方法可以获取其名称。
故一般步骤为:
- 先创建 JMenuBar。
- 再创建 JMenu。
- 再创建 JMenuItem。
- 把 JMenuItem 放在 JMenu 当中。
- 把 JMenu 放在 JMenuBar 当中。
- 最后再把 JMenuBar 放在整个 JFrame 当中。
import javax.swing.*;
public class GameJFrame extends JFrame {
//表示游戏主界面
public GameJFrame() {
//设置菜单界面基本信息的代码这里忽略不写
//初始化菜单
//创建整个菜单对象
JMenuBar jMenuBar = new JMenuBar();
//创建两个JMenu对象
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
//创建JMenuItem对象
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
//将每一个JMenuItem添加到JMenu中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
//给JMenuItem绑定事件
replayItem.addActionListener(this);
reLoginItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
//将每一个JMenu添加到JMenuBar中
jMenuBar.add(functionJMenu);
jMenuBar.add(aboutJMenu);
//将JMenuBar添加到JFrame中
this.setJMenuBar(jMenuBar);
//界面展示
this.setVisible(true);
}
}
JLabel
JLabel 是一个用于处理文字或者图片的容器。
处理图片:
- 创建一个图片 ImageIcon 的对象。
- 创建一个 JLabel 对象。
- 把管理容器添加到界面中。
图片是以左上角为原点,向右为x轴正方向,向下为y轴正方向。图片的坐标看的是左上角的坐标位置。先加载的图片在上方,后加载的图片会在下方。
import javax.swing.*;
public class GameJFrame extends JFrame {
//表示游戏主界面
public GameJFrame() {
//...
//清空原本已经出现的所有图片
this.getContentPane().removeAll();
//创建ImageIcon对象
ImageIcon icon = new ImageIcon("图片路径");
//创建JLabel对象
JLabel jLabel = new JLabel(icon);
//指定位置
jLabel.setBounds(0, 0, 105, 105); //记得取消居中放置
//给图片添加边框
jLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
//把管理容器放入界面中,需要先获取出隐藏的用于放置组件的容器
this.getContentPane().add(jLabel);
//刷新界面
this.getContentPane().repaint();
//界面展示
this.setVisible(true);
}
}
处理文字:处理文字时不需要ImageIcon
,只需要直接在JLabel
对象的构造方法当中填入要打印的文字即可。
事件
事件是可以被组件识别的操作,当我们对组件执行了某些操作之后,组件可以执行某段代码。
事件源:哪些组件会被操作,例如:按钮、图片、窗体。
事件:某些操作,例如:鼠标点击、鼠标划入。
绑定监听:当事件源上发生了某个事件之后,则执行某段代码。监听有:键盘监听(KeyListener)、鼠标监听(MouseListener)、动作监听(ActionListener,动作监听只会监听鼠标左键点击和键盘空格)。
动作监听
import javax.swing.*;
import java.awt.event.ActionEvent;
public class Test1 {
public static void main(String[] args) {
//设置窗口
JFrame jFrame = new JFrame();
jFrame.setSize(603, 680);
jFrame.setTitle("事件演示");
jFrame.setAlwaysOnTop(true);
jFrame.setLocationRelativeTo(null);
jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jFrame.setLayout(null);
//创建一个按钮对象
JButton jtb = new JButton("点我啊");
//设置宽高
jtb.setBounds(0, 0, 100, 50);
//添加动作监听
//jtb:组件对象,表示要给哪个组件添加事件
//addActionListener:表示我要给组件添加哪个事件监听(动作监听鼠标左键点击,空格)
jtb.addActionListener(e -> System.out.println("不要点我~"));
//把按钮添加到界面当中
jFrame.getContentPane().add(jtb);
jFrame.setVisible(true);
}
}
也可以直接利用本类去继承 ActionListener 来起到监听自身方法的作用:
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
public class MyJFrame extends JFrame implements ActionListener {
//创建一个按钮对象
JButton jtb1 = new JButton("点我啊");
//创建一个按钮对象
JButton jtb2 = new JButton("再点我啊");
public MyJFrame() {
//设置窗口
this.setSize(603, 680);
this.setTitle("事件演示");
this.setAlwaysOnTop(true);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setLayout(null);
//给按钮设置宽和高
jtb1.setBounds(0, 0, 100, 50);
//给按钮添加事件
jtb1.addActionListener(this);
//给按钮设置宽和高
jtb2.setBounds(100, 0, 100, 50);
jtb2.addActionListener(this);
//按钮添加到整个界面中
this.getContentPane().add(jtb1);
this.getContentPane().add(jtb2);
//让整个页面显示出来
this.setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
//对当前的按钮进行判断
//利用getSource获取当前被操作的对象
Object source = e.getSource();
if(source == jtb1) {
jtb1.setSize(200, 200);
}
else if(source == jtb2) {
Random r = new Random();
jtb2.setLocation(r.nextInt(500), r.nextInt(500));
}
}
}
鼠标监听
鼠标点击一共分为三步:挪到事件源上还没有点(划入动作)、用鼠标点击但是不松(按下动作)、松开鼠标(松开动作)、将鼠标从事件源上挪出(划出动作),MouseLisenter 将按下和松开两个动作归为一类,称为单击动作。
import javax.swing.*;
import java.awt.event.*;
public class MyJFrame extends JFrame implements MouseListener {
//创建一个按钮对象
JButton jtb1 = new JButton("点我啊");
public MyJFrame() {
//设置窗口
this.setSize(603, 680);
this.setTitle("鼠标监听");
this.setAlwaysOnTop(true);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setLayout(null);
//给按钮设置位置和宽高
jtb1.setBounds(0, 0, 100, 50);
//给按钮绑定鼠标事件
jtb1.addMouseListener(this);
//添加按钮到界面中
this.getContentPane().add(jtb1);
//显示界面
this.setVisible(true);
}
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("单击");
}
@Override
public void mousePressed(MouseEvent e) {
System.out.println("按下不松");
}
@Override
public void mouseReleased(MouseEvent e) {
System.out.println("松开");
}
@Override
public void mouseEntered(MouseEvent e) {
System.out.println("鼠标划入");
}
@Override
public void mouseExited(MouseEvent e) {
System.out.println("鼠标划出");
}
}
键盘监听
利用keyPressed
监听键盘的键按下,利用keyReleased
监听键盘的键被释放。
package cn.edu.hnu.test;
import javax.swing.*;
import java.awt.event.*;
public class MyJFrame extends JFrame implements KeyListener {
public MyJFrame() {
//设置窗口
this.setSize(603, 680);
this.setTitle("鼠标监听");
this.setAlwaysOnTop(true);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setLayout(null);
//一般是给整个窗体添加键盘监听
this.addKeyListener(this);
//显示界面
this.setVisible(true);
}
@Override
public void keyTyped(KeyEvent e) {
}
/*
* 如果我们按下一个按键不松开,则会重复调用该方法
* 可以利用getKeyCode方法得到按键对应的编号,用来区分按键
* */
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
System.out.println("按下不松 " + code);
}
@Override
public void keyReleased(KeyEvent e) {
System.out.println("松开按键");
}
}
JDialog
弹框组件。可以放置图片,放置图片时依旧需要使用JLabel
。
public class Main {
public static void main(String[] args) {
//创建一个弹框
JDialog jDialog = new JDialog();
//创建一个JLabel
JLabel jLabel = new JLabel(new ImageIcon("图片路径"));
//设置位置,表示将图片放在弹框的那个位置,是相对弹框而言的
jLabel.setBounds(0, 0, 258, 258);
//把图片添加到弹框中
jDialog.getContentPane().add(jLabel);
//给弹框设置大小
jDialog.setSize(344, 344);
//让弹框置顶
jDialog.setAlwaysOnTop(true);
//让弹框居中
jDialog.setLocationRelativeTo(null);
//弹框不关闭则无法操作下面的界面
jDialog.setModal(true);
//让弹框显示出来
jDialog.setVisible(true);
}
}
JDK 新特性
Java Record
在 Java 中,Record 是一种特殊类型的 Java 类。可用来创建不可变类,语法简短,可以认为 Record 是一个内置的 Lombok,减少样例代码。Record 类是语言级别的,充当“数据载体”,用于在类和应用程序中进行数据传输。
Java Record 避免样板代码,具有如下特点:
- 带有全部参数的构造方法。
- public 访问器。
- toString、hashCode、equals 方法。
- 没有 get、set 方法,没有遵循 Bean 的命名规范。
- final 类,不能继承 Record,Record 为隐式的 final 类。
- 不可变类,通过构造创建 Record。
- final 属性,不可修改。
- 不能声明实例属性,能声明 static 成员。
基本使用
// 创建record类型,指定参数
public record Student(Integer id, String name, String email, Integer age) {
}
@Test
public void test01() {
// 创建record对象和创建普通java对象一样
Student stu1 = new Student(1001, "李四", "lisi@qq.com", 18);
System.out.println(stu1);
// 获取属性值需要通过公共访问器,只读,不能写
// 注意record的命名不遵循bean的规范
System.out.println(stu1.id());
System.out.println(stu1.name());
System.out.println(stu1.email());
System.out.println(stu1.age());
Student stu2 = new Student(1002, "张三", "zhangsan@qq.com", 20);
System.out.println(stu2);
// 内置了equals还有hashCode等方法
System.out.println(stu1.equals(stu2));
}
Instance Methods
Record 类型也可以在其中定义方法:
public record Student(Integer id, String name, String email, Integer age) {
// 创建实例方法
public String concat() {
return "姓名是:" + this.name + " 年龄是:" + this.age;
}
// 创建静态方法
public static String toUpperCase(String text) {
return text.toUpperCase();
}
}
构造方法
Record 中有三种类型的构造方法,分别是:紧凑型、规范型、定制构造。
// 默认是规范型构造方法
public record Student(Integer id, String name, String email, Integer age) {
// 紧凑构造方法,在编译的时候会和规范型构造方法合并到一起
public Student {
System.out.println("id = " + id);
if (id < 1) {
throw new RuntimeException("id < 1");
}
}
// 定制构造方法
public Student (Integer id, String name) {
this(id, name, null, null);
}
}
实现接口
Record 类型一样也可以实现接口:
// 定义接口
public interface PrintInfo {
void printInfo();
}
// 实现接口
public record Student(Integer id,
String name,
String email,
Integer age) implements PrintInfo {
@Override
public void printInfo() {
System.out.println("printInfo");
}
}
Local Record
Record 类型可以作为局部变量来使用,在代码块中定义 Record,然后直接进行使用:
public static void main(String[] args) {
// 定义局部record
record SaleRecord(Integer saleId, String productName, Double money){}
// 使用局部record
SaleRecord saleRecord = new SaleRecord(1001, "显示器", 549.23);
System.out.println(saleRecord);
}
嵌套 record
public record Address(String city, String address, String zipcode) {}
public record PhoneNumber(String areaCode, String number) {}
// Customer嵌套了上述两个record
public record Customer(String id, String name, PhoneNumber phoneNumber, Address address) {}
两个其他方法
@Test
public void test03() {
Student stu = new Student(2, "张三");
// isRecord()判断是否为Record类型
boolean record = stu.getClass().isRecord();
System.out.println(record);
// getRecordComponents()获取记录类的所有组件
RecordComponent[] recordComponents = stu.getClass().getRecordComponents();
Arrays.stream(recordComponents).forEach(System.out::println);
}
Switch
支持箭头表达式
public static void main(String[] args) {
// switch的箭头表达式
Scanner sc = new Scanner(System.in);
int week = sc.nextInt();
String memo = "";
switch (week) {
case 6, 7 -> memo = "休息日";
case 1, 2, 3, 4, 5 -> memo = "工作日";
default -> memo = "输入错误";
}
System.out.println(memo);
}
支持 yield 返回值
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int week = sc.nextInt();
// yield能将值直接返回出去,yield跳出当前switch块
String memo = switch (week) {
case 6, 7 : yield "休息日";
case 1, 2, 3, 4, 5 : yield "工作日";
default : yield "输入错误";
};
System.out.println(memo);
}
文本块
Text Block 可以处理多行文本,无需使用+
连接,表达很方便。
// error,不能把文本块放在一行上
String text1 = """ zhangsan """;
// error,文本块第一个三引号后需要跟换行符
String text2 = """ zhangsan
""";
// 正确写法
String text3 = """
zhangsan
""";
// 使用\可以将换行效果去除,tab缩进效果同样也会作用到文本上
String text = """
hello \
world
""";
System.out.println(text);
var
var 是一个保留字(类似于 C++ 的 auto),不是关键字。方法内声明的局部变量必须有初值。var 动态类型是编译器根据变量所赋的值来推断类型,可以减少不必要的排版,使得代码简洁。
使用 var 的时机:
- 简单的临时变量。
- 复杂,多步骤逻辑,嵌套的表达式等,简短的变量有助于理解代码。
- 能够确定变量初始值。
- 变量类型比较长时可以使用 var 简化书写。
// 通常
try (Stream<Customer> result = dbconn.executeQuery(query)) {}
List<Customer> custs = new ArrayList();
for(Customer cust : custs) {}
// 推荐
try (var result = dbconn.executeQuery(query)) {}
List<Customer> custs = new ArrayList();
for(var cust : custs) {}
Sealed
Sealed Class,密闭类,其主要目的是为了限制继承,避免出现类与类之间不可预见的异常。
// 声明父类,permits指定哪些类可以继承本类
public sealed class Shape permits Circle, Square, Rectangle {
//...
}
// 声明子类
/*
final 终结,依然是密闭的,无法再继承
sealed 子类依旧是密闭类,需要子类实现
non-seacled 非密闭类,扩展使用,不受限
*/
// 终结,无法再被继承
public final class Circle extends Shape {}
// 依旧是密闭类,需要子类实现
public sealed class Square extends Shape permits RoundSquare {}
public final class RoundSquare extends Square {}
// 放弃密闭,可以被扩展
public non-sealed class Rectangle extends Shape {}
public class Line extends Rectangle {}
// 声明密闭接口
public sealed interface SomeService permits SomeServiceImpl {
void doThing();
}
// 实现接口
public final class SomeServceImpl implements SomeService {
@Override
public void doThing() {
}
}
《Java 基础》系列到此也就全部完结啦,全文上下共 6w 字左右!(完结撒花~☆: .。. o(≧▽≦)o .。.:☆)