计算机基础知识
一、快捷键
- Ctrl+C:复制
- Ctrl+V:粘贴
- Ctrl+A:全选
- Ctrl+X:剪切
- Ctrl+Z:撤销
- Ctrl+S:保存
- Alt+F4:关闭窗口
- Shift+Dlete:永久删除
- Win+D:回到桌面
- Win+E:打开我的电脑
- Win+R:打开运行
- Ctrl+Shift+Esc:打开任务管理器
- Win+Tab:切换应用
- Win+R 输入cmd:打开cmd
二、常用Dos命令
- 盘符切换
- 查看当前目录下所有文件 dir
- 切换目录 cd change directory->cd /d directory
- 返回上一级 cd..
- 清理屏幕 cls(clear screen)
- 退出终端 exit
- 查看版本 java -version
- ping 命令
ping www.baidu.com
- 文件操作
md(创建目录)
rd(移除目录)
cd>(创建文件)
del(删除文件)
三、Markdown语法详解
视频教程
(一)jdk安装与环境配置
jdk安装和配置环境教程视频
(二)IntelliJ IDEA
Ⅰ.开发工具(IntelliJ IDEA)安装
idea破解版安装教程视频
具体步骤
Ⅱ.idea开发java程序步骤
- project(工程)->module(模块)->package(包)->class(类)
- New project/module/package/class
- 编译后的class文件在工程路径下的一个out文件夹里
Ⅲ.idea快捷键
![屏幕截图 2025-08-14 154756]()
Ⅳ.idea其他操作
视频教程
- @Override(Override注解),它可以指定java编辑器,检查我们方法重写的格式是否正确
- //lombok技术可以实现为类自动添加getter setter方法、无参构造器、toString方法等。
(1)@Data //@Date注解可以自动生成getter setter方法、无参构造器、toString方法等
(2)@NoArgsConstructor //自动生成无参构造器
(3)@AllArgsConstructor //自动生成有参构造器
- @FunctionalInterface //声明函数式接口的注解。标记函数式接口,编译器会检查,如果接口中有多个抽象方法,编译器会报错。
(三)Java基础语法
Ⅰ.注释
- 单行注释://
- 多行注释:/**/
- 文档注释:/** */
Ⅱ.数据类型
- 基本数据类型:4大类8种
![屏幕截图 2025-08-14 220834]()
- 引用数据类型:String
- 随便写的整数字面量默认是int类型,加上L/l就是long类型的数据了
- 随便写的小数字面量默认是double类型,加上F/f就是float类型的数据了
Ⅲ.标识符
- 标识符就是名字
- 标识符规则:由数字,字母,下划线,美元符等组成,且不能数字开头,不能用关键字,不能用特殊符号(&,%···)
Ⅳ.方法的一些注意事项
- 方法可以重载
- 一个类中,出现多个方法的名称相同,但它们的形参列表是不同的,那么这些方法就称为方法重载。(通过方法名称标记同一功能,通过参数进行差异化。)
- 无返回值的方法中可以直接通过单独的return;立即结束当前方法的执行。
Ⅴ.自动-强制类型转换
- 为什么要进行类型转换
- 什么是自动类型转换
- 什么是强制类型转换
- 默认情况下,大范围类型的变量直接赋值给小范围类型的变量会报错
- 可以强行将类型范围大的变量、数据赋值给类型范围小的变量
数据类型 变量=(数据类型)变量、数据
- 强制类型转换有哪些需要注意的
- 大转小可能出些数据丢失
- 小数强制转换成整数是直接截断小数保留整数
Ⅵ.表达式的自动类型提升
- 小范围的类型会自动转换成大范围的类型运算
- 最终类型由表达式中的最高类型决定
- byte short char式直接转换成int类型参与运算的
Ⅶ.输入-输出
- 输入:System.out.println("");
- 输出
(1) 导包(告诉程序去JDK的哪个包中找扫描器技术):import java.util.Scanner;
(2) 得到键盘扫描器对象(东西):Scanner sc=new Scanner(System.in);
(3) 等待接收用户输入数据:String name=sc.next();/int age=sc.nexitInt();
Ⅷ.逻辑运算符
- &:有一个为false、结果是false
- &&:有一个为false、结果是false,但前一个为false,后一个条件就不执行了
- |:有一个为true、结果是true
- ||:有一个为true、结果是true,但前一个为true,后一个条件就不执行了
- ^:相同是false、不同是true
Ⅸ.switch分支-穿透性
- switch分支结构
点击查看代码
switch(表达式){
case 值1:
执行代码...;
break;
case 值2:
执行代码...;
break;
...
case 值n-1:
执行代码...;
braek;
default:
执行代码n;
}
- 注意事项
- 表达式类型只能是byte、short、int、char,JDK5开始1支持枚举,JDK7开始支持String、不支持double、float、long。
- case给出的值不允许重复,且只能是字面量,不能是变量。
- 正常使用switch的时候,不要忘记写break,否则会出现穿透现象。
- 存在多个case分支的代码是一样时,可以把代码写到一个case块,其他case块通过穿透性能,穿透到该case块即可,这样可以简化代码。
Ⅹ.三种循环的使用(for、while、do-while)
for循环中,控制循环的变量只在循环中使用。while循环中,控制循环的变量在循环后还可以继续使用。
ⅩⅠ.跳转关键字
- break:跳出并结束当前所在循环的执行。
- 在Java中,带标签的break语句(如break OUT;)用于从多层嵌套的循环或代码块中直接跳出到指定的外层标签位置。
点击查看代码
标签名:
for (初始化; 条件; 迭代) {
// 外层循环
for (初始化; 条件; 迭代) {
// 内层循环
if (某种条件) {
break 标签名; // 直接跳出到标签指定的循环之后
}
}
}
// 跳转到这里
- continue:用于跳出当前循环的当次执行直接进入循环的下一次执行。
ⅩⅡ.生成随机数
- Math.random()反回[0.0,1.0)的随机数字,Math.random()返回一个double类型的值。
- import java.util.Random;
Random r=new Random();//得到一个随机数对象
int luckNumber=r.nextInt(100)+1;//[0,99]+1=>[1,100]
ⅩⅢ.开平方
Math.sqrt(number)
ⅩⅣ.数组
- 静态初始化数组,定义已经确定了数据
数据类型[] 数组名={元素1,元素2,元素3,…};
如:int[] arr={12,24,36};
- 完整格式
数据类型[] 数组名=new 数据类型[]{元素1,元素2,元素3,…};
如:int[] arr=new int[]{12,24,36};
- "数据类型[] 数组名"也可写成"数据类型 数字名[]"的形式,定义数组的三种方法
1.String[] names={"",""};
2.String names[]={"",""};
3.String[] names=new String[]{"",""};
- names.length: 获取数组的长度(元素个数)
- 动态初始化数组定义数组时先不存入具体的元素,只确定数组存储的数据类型和数组的长度
数据类型[] 数组名=new 数据类型[长度];
int[] arr=new int[3];
- 动态初始化数组元素默认值规则:
![屏幕截图 2025-08-25 165406]()
(四)面向对象编程
面向对象三大特征:封装、继承、多态。
- 对象
特殊的数据结构,一个实体,有属性和行为。也可以理解为一张表。
- 类(对象类)
特殊的数据结构,有属性和行为。类只在计算机中加载一次。
- 内存分配
java在内存的JVM虚拟机上运行,JVM虚拟机又分为堆内存、栈内存和方法区一同执行程序。
变量存在栈里,变量指向对象,对象存在堆里,对象指向类,类存在方法区,将方法区中的方法调到栈中执行
万物皆对象,一个数据由一个对应的对象处理,我们设计对象时就是在设计类(对象的模板)。
Ⅰ.类的基本语法
- 构造器
(1)构造器(分为无参构造器和有参构造器):类中定义的方法,用来初始化对象,在类中定义的方法,称为方法,在类中定义的变量,称为属性。 名字与类名一致,无返回值,无返回值类型,无访问修饰符。
(2)特点:创建对象时,对象会自动调用构造器,如果没有定义构造器,JVM会自动生成一个无参构造器。
(3)应用场景:创建对象时,调用构造器,立即初始化对象成员变量的值。
(4)注意:类默认有一个无参构造器(没有显示而已),若你自己定义了有参构造器,那么类默认的无参数构造器就没有了,此时如果还想用无参数构造器,就必须自己手写一个无参构造器出来。
- this关键字
(1)this 关键字:是一个变量,可以用在方法中,用来拿到当前对象;哪个对象调用方法,this就指向哪个对象,也就是拿到哪个对象。
(2)应用场景:用来解决对象的成员变量与方法内部变量的名称一样时,导致访问冲突问题的。
点击查看代码
public void print(String name){//this关键字解决变量冲突问题
System.out.println(name+this.name);//this.name拿到的是对象变量(成员变量)name,而不是局部变量name
}
- 封装
(1)就是用类设计对象处理某一个事物的数据时,应该把要处理的数据,以及处理这些数据的方法,设计到一个对象中去。
(2)封装的设计要求:合理隐藏、合理暴露。
(3)(合理隐藏)使用private(私有、隐藏)关键字进行修饰,防止用户在其他类中随意对本类内的变量修改数据,只允许在本类中直接被访问。
(3)(合理暴露)使用get和set方法,用public(公开)进行修饰,让用户在类外直接调用,修改数据。
- 实体类(JavaBean)
(1)实体类:仅仅只是一个用来保存数据的java类,可以用它创建对象,保存某个事物的数据。
(2)特点:成员变量必须私有,提供get和set方法;必须有无参构造器。
(3)应用场景:实体类的对象只负责数据的封装,不涉及任何业务逻辑。而数据的业务处理交给其他类的对象来完成,以实现数据和业务处理相分离(解耦)。
- static修饰成员变量
(1)static关键字:叫静态,可以修饰成员变量、成员方法。
(2)静态变量(类变量):有static修饰,属于类,在计算机里只有一份,被类的全部对象共享,所有对象都可以访问。
类名.静态变量(推荐)
对象名.静态变量(不推荐)
(3)实例变量(对象的变量):没有static修饰,属于每个对象,每个对象都有自己的变量,对象可以访问。
对象.实例变量
(4)应用场景:如果某一个数据只需要一份,并且希望能够被共享(访问、修改),则该数据被定义成静态变量。 (如用户类,记录了创建了多少个用户对象)
(5)访问自己类中的类变量,可以省略类名不写。
注意:在某个类中访问其他类里的类变量,必须带类名。
- static修饰方法
(1)(有static修饰)静态方法:有static修饰的成员方法,属于类,最好用类名调用,少用对象名调用。
(2)(无static修饰)实例方法:无static修饰的成员方法,属于对象,用对象名调用,不能用类名调用。
规范:如果一个方法只是为了做一个功能且不需要直接访问对象的数据,这个方法直接定义为静态方法。
如果这个方法是关于对象的行为,需要访问对象的数据,这个方法必须定义为实例方法。
(3)比如在main方法中,我们直接调用其他方法,是用类名调用,只不过类名在同一类中可以忽略不写。(main方法是静态方法)
- 工具类与静态方法
(1)工具类:封装了多个静态方法,每个方法用来完成一个功能,给开发人员直接使用。
(2)区别:实例方法需要创建对象来调用,此时对象占用内存,而静态方法不需要,可以减少内存消耗;静态方法可以用类名调用,调用方便,能节省内存。
点击查看代码
public class StaticAbout {//工具类
//工具类没有创建对象的必要性,建议将工具类的构造器私有化。
private StaticAbout() {
}
public static String getCode(int n) {//静态方法与工具类
String code = "";
for (int i = 0; i < n; i++) {
int type = (int) (Math.random() * 3);//0-9 1-26 2-26
switch (type) {
case 0:
code += (int) (Math.random() * 10);
break;
case 1:
code += (char) (Math.random() * 26 + 'a');//得到小写字母的区间
break;
case 2:
code += (char) (Math.random() * 26 + 'A');
}
}
return code;
}
}
- 静态方法与实例方法访问注意事项
(1)静态方法中可直接访问静态成员,不可直接访问实例成员。
(2)实例方法中即可直接访问静态成员,也可直接访问实例成员。
(3)实例方法中可以出现this关键字,静态方法中不可出现this关键字。
点击查看代码
public class Attention{
public static int count=100;//静态变量
public static void print(){//静态方法
System.out.println("Hello World!");
}
public String name;//实例变量,属于对象
public void prints(){//实例方法,属于对象
}
public static void main(String[] args) {
}
//(1)、静态方法中可直接访问静态成员,不可直接访问实例成员。
public static void printTest1(){
System.out.println(count);
print();
//System.out.println(name);//报错
//prints();//报错
//System.out.println(this);//报错,this代表的只能是对象
System.out.println(Attention.count);
}
//(2)、实例方法中即可直接访问静态成员,也可直接访问实例成员。
public void printTest2(){
System.out.println(count);
print();
System.out.println(name);
prints();
System.out.println(this);//实例方法中,this代表的是当前对象
}
}
Ⅱ.面向对象高级
一)继承
- 继承
(1)继承关键字:extends,可以让一个类与另一个类建立起父子关系
public calss B extends A{}//其中A是父类,B是子类
(2)只能继承父类的非私有成员(成员变量、成员方法)
(3)继承后的创建:子类的对象是由子类、父类共同完成的。
(4)继承的好处:代码重用,减少代码量,提高效率。
- 权限修饰符
用来限制类中的成员(成员变量、成员方法、构造器)能够被访问的范围。
| 修饰符 |
本类里 |
同一个包中的其他类 |
子孙类(包括不同包的) |
任意类 |
| private |
√ |
|
|
|
| 缺省 |
√ |
√ |
|
|
| protected |
√ |
√ |
√ |
|
| public |
√ |
√ |
√ |
√ |
点击查看代码
public class Father {
private void privateMethod() {//二、2、权限修饰符之私有方法
System.out.println("private method");
}
void defaultMethod() {//二、2、权限修饰符之缺省方法
System.out.println("default method");
}
protected void protectedMethod() {//二、2、权限修饰符之受保护方法
System.out.println("protected method");
}
public void publicMethod() {//二、2、权限修饰符之公共方法
System.out.println("public method");
}
public static void main(String[] args) {//本类中可以调用的所有方法
Father father = new Father();
father.privateMethod();
father.defaultMethod();
father.protectedMethod();
father.publicMethod();//所有类都可以调用public方法
}
}
//以下为同一包的子类
class SonA extends Father {
public void test() {
//privateMethod();//子类中不可以访问父类的私有方法
defaultMethod();//子类中可以访问父类的缺省方法
protectedMethod();//子类中可以访问父类的受保护方法
publicMethod();//子类中可以访问父类的公共方法
}
}
//以下为不同包的子类
class SonC extends Father {
public void test() {
//privateMethod();//子类中不可以访问父类的私有方法
//defaultMethod();//不同包的子类中可以访问父类的缺省方法
protectedMethod();//不同包的子类中可以访问父类的受保护方法
publicMethod();//不同包的子类中可以访问父类的公共方法
}
}
//以下为不同包的非子类
class main {
public static void main(String[] args) {
Father father = new Father();
//father.privateMethod();
//father.defaultMethod();
//father.protectedMethod();
father.publicMethod();//其他包中,只能访问public方法
}
}
- 继承的特点
| 单继承 |
一个类只能继承一个直接父类 |
| 多层继承 |
不支持多继承,但支持多层继承(多继承的话若两个父类出现相同方法,无法判断调用哪个) |
| 祖宗类 |
Java中的所有类都是Object类的子类(一个类要么直接继承Object,要么默认继承Object,要么间接继承) |
| 就近原则 |
优先访问自己类中,自己类中的没有才访问父类,父类也没有就报错(访问父类的成员要加super) |
点击查看代码
public class TestFeature{//测试类
//二、3、继承的特点
public static void main(String[] args) {
zilei zi=new zilei();
zi.test();
}
}
class fulei{
String name="FUlei";
public void run(){
System.out.println("fuleiRun");
}
}
class zilei extends fulei{
String name="ZiLei";
public void run(){
System.out.println("zileiRun");
}
public void test(){
String name="ZiLeiTEST";
System.out.println(name);//test的name
System.out.println(this.name);//zilei的name
System.out.println(super.name);//fulei的name
run();//zilei的run优先级更高
this.run();//zilei的run
super.run();//fulei的run
}
}
- 方法重写(声明不变,重新实现)
(1)重写小技巧:使用Override注解(@Override),它可以指定java编辑器,检查我们方法重写的格式是否正确,代码的可读性也会更好。
(2)方法重写:子类写了一个方法名称,形参列表与父类某个方法一样的方法去覆盖父类的该方法。
(3)子类重写父类方法时,访问权限必须大于或等于父类的该方法的权限(public>protected>缺省)
(4)重写的方法返回值类型,必须与被重写方法的返回值类型一样,或者范围更小。
(5)私有方法(不能被继承所以不能被重写)和静态方法(自己调用)不能被重写,否则报错。
点击查看代码
public class TestOverride {
public static void main(String[] args) {//二、4、方法重写
animal a=new cat();
//用父类申明对象,利用多态性和向上转型的概念,以实现更加灵活和可扩展的程序设计,降低代码的耦合性
a.eat();
cat b=new cat();
b.eat();
}
}
class animal{
public void eat(){System.out.println("animal eat");}
}
class cat extends animal{
@Override//方法重写的校验注解(标志),要求重写的方法与父类方法签名一致,否则编译报错,可读性好
public void eat(){System.out.println("cat eat!!!");}
}
- 重写toString方法
点击查看代码
public class TestOverride {
public static void main(String[] args) {
animal a=new animal();
System.out.println(a);
//若没重写toString方法,则返回:com.rasion.extendANDpolymorphism.extend.cat@4e50df2e
System.out.println(a.toString());
//我们直接输出对象,会默认调用Object的toString方法,返回对象的地址信息
//故我们可以重写toString方法,返回我们想要的信息
}
}
class animal{
private String name;
@Override
public String toString() {//重写toString方法,返回我们想要的信息
return "animal{" +
"name='" + name + '\'' +
'}';
}
}
- 子类构造器
(1)子类构造器,必须先调用父类的构造器,再执行自己的构造器。(可以用super(···)指定调用父类的有参构造器)
(2)默认情况下,子类构造器第一行代码都是super()(写不写都有),他会调用父类的无参构造器,若父类没有无参构造器,则报错。
(3)如果父类没有无参构造器,则我们必须在子类构造器的第一行手写super(···),指定去调用父类的有参数构造器。
(4)应用场景:有一些参数是处在父类中,子类没有,但子类需要用到,这时,子类构造器中,可以先调用父类的有参构造器,为对象中包含父类这部分的成员变量进行赋值,再调用子类的构造器。
- 构造器用this(...)调用兄弟构造器(实现填写默认信息)
(1)一般调用兄弟构造器(在构造器中调用本类的其他构造器),可以实现代码复用。
(2)注意:super(..) this(...)必须写在构造器第一行,并且不能同时出现。(兄弟构造器只需要一个调用父类构造器即可)
点击查看代码
public class User{
private String name;
private int age;
private String school;
//...此处省略get、set方法toString方法
public User(String name, int age) {
this(name, age, "黑马");//调用本类其他构造器,以实现填写默认信息
}
public User(String name, int age, String school) {
this.name = name;
this.age = age;
this.school=school;
}
}
二)多态
- 多态
(1)多态是在继承/实现的情况下的一种现象,表现为:对象多态、行为多态(对象的多样性,行为的多样性,但是没有成员变量的多态性)
(2)多态前提:有继承/实现关系;存在父类引用子类对象;存在方法重写。
(3)注意:多态是对象、行为的多态,java中的属性(成员变量)不谈多态。
点击查看代码
people p1=new student();//对象多态
p1.run();//行为多态
people p2=new teacher();//对象多态
p2.run();//方法:编译看左边,运行看右边
System.out.println(p2.name);//成员变量:编译看左边,运行也看左边
- 多态的好处、问题
(1)在多态形式下,右边的对象是解耦合的,更便于扩展和维护。
(2)定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
(3)问题:多态下不能使用子类的独有功能。
点击查看代码
public class TestPolymorphism {
public static void main(String[] args) {
people p1=new student();//多态调用不了子类的独有功能
people p2=new teacher();
show(p1);
show(p2);
System.out.println(p1.name);//成员变量:编译看左边,运行也看左边
}
public static void show(people p) {
//父类类型作为参数,可以接收一切子类变量
System.out.println("=====+++=====");
p.run();//调用方法:编译看左边,运行看右边
}
}
- 多态下的类型转换(instanceof判断)
(1)自动类型转换:父类 变量名=new 子类();例如:People p=new Teacher();
强制类型转换:子类 变量名=(子类) 父类变量;例如:Teacher t=(Teacher)p;
(2)可以把对象转换成其真正的类型,从而解决了多态下不能调用子类独有方法的问题。
(3)存在继承/实现时,就可以进行强制类型转换,编译阶段不会报错。
但在运行时,如果发现对象的真实类型与强制转换后的类型不同,就会报类型异常(ClassCastException)的错误。
(4)在强转前可以用instanceof关键字判断对象的真实类型,再进行强转:对象 instanceof 类型。
点击查看代码
public class TestPolymorphism {
public static void main(String[] args) {
people p1=new student();
people p2=new teacher();
student s=(student)p1;//强制类型转换
//teacher t=(teacher)p1;//转换错误,因为p1是student类型
//编译阶段有类型强转不会报错,运行阶段会报错
show(p1);
show(p2);
}
public static void show(people p) {
if(p instanceof teacher) {//判断类型,一般会在方法中写,来判断p是否为teacher类型
teacher t=(teacher)p;
...//调用teacher的独有功能
}else if(p instanceof student) {
student s=(student)p;
...//调用student的独有功能
}
}
}
三)包
- 包就是文件夹,用来管理各种不同功能的java类。
- 包名的书写规则:公司域名反写+包的作用,需要全部英文小写,见名知意。
- 全类名:包名+类名
- 导包
(1)使用同一个包中的类时,不需要导包。
(2)使用java.lang包中的类时,不需要导包。
(3)其他情况都需要导包。
(4)如果同时使用两个包中的同名类,需要用全类名。
四)final(最终的)
- final关键字用来修饰类、方法、变量。
(1)修饰类:代表该类不能被继承,为最终类。(一般是用在工具类中)
(2)修饰方法:代表该方法不能被重写,为最终方法。
(3)修饰变量(局部变量、成员变量(静态、实例)):代表该变量有且仅能被赋值一次。(一般是修饰静态变量)
(4)final修饰基本类型(int,double,boolean,char...)的变量,变量存储的数据不能被改变。
(5)final修饰引用类型(String,Array...)的变量,变量存储的地址不能被改变,但地址所指向的对象的内容是可以被改变的。
2、常量
(1)使用了static final修饰成员变量为常量。
(2)作用:通常使用常量记录系统配置信息;常量都统一到常量包内(Constant),方便修改。
(3)注意!常量名的命名规范:建议使用大写英文单词,多个单词使用下划线连接起来。
(4)程序编译后,常量会被“宏替换”:出现常量的地方全部会被替换成其记住的字面量,减少内存消耗,这样可以保证使用常量和直接使用字面量的性能是一样的。
点击查看代码
public class Constant {//常量包
public static final String NAME = "rasion";
}
五)单例类(设计模式)
- 什么是设计模式:程序中一个问题n种解法中的最优解(设计模式主要学:解决什么问题、怎么写)(设计模式有20多种,对应20多种软件开发中会遇到的问题)
- 单例设计模式
(1)作用:确保某个类只有只能创建一个对象。
(2)把类的构造器私有。(确保单例类对外不能创建太多对象,单例才有可能性)
(3)定义一个类变量存储本类的一个唯一对象。
(4)定义一个类方法,返回这个类的唯一对象。
- 单例应用场景:任务管理器对象、获取运行时对象(有且仅需要一个对象)
- 饿汉式单例类:在获取类的对象时,对象已经创建好了。
点击查看代码
public class A {//饿汉式单例类:拿对象的时候对象早已生成,无论是用不用,都会创建。
private A(){}//1、私有化构造器
private static A a=new A();//2、实例化私有静态变量
//当类被加载时,静态变量a会被初始化,此时类的私有构造函数会被调用。这时候,单例类的唯一实例就被创建出来了。
public static A getObject(){
return a;
}//3、定义一个类方法返回对象
}
- 懒汉式单例类:用对象时,才开始创建对象(延迟加载对象)。
(1)把类的构造器私有。
(3)定义一个静态(类)变量用于存储对象。
(4)提供一个静态(类)方法,保证返回的是同一个对象。
点击查看代码
public class B {//懒汉式单例类:拿对象的时候才创建,真正需要对象时才开始创建。
private B(){}//1、私有化构造器
private static B b;//2、定义一个类变量用于存储对象
public static B getInstance(){//提供一个类方法返回类的唯一对象
if(b==null){
//第一次拿对象时,会创建对象,给静态变量b记住。
b=new B();
}
return b;
}
}
六)枚举类(一种特殊类)
- 枚举类的写法:
修饰符 enum 枚举类名{
名称1,名称2,名称3;
其他成员...
}
- 特点:
(1)枚举类中的第一行,只能写枚举类的对象名称,且要用逗号隔开。
(2)这些名称,本质是常量,每个常量都记住了枚举类的一个对象。
(3)枚举都是最终类,不可以被继承,枚举类都是继承java.lang.Enum类的。
点击查看代码
public enum A {//枚举类
X,Y,Z;//枚举类第一行只能罗列一些名称,这些名称都为常量,并且每个常量会记住枚举类的一个对象
}
Compiled from "A.java"//对A.java的反编译
public final class A extends java.lang.Enum<A>{//枚举类继承了Enum
public static final A X=new A();//枚举类第一行只能罗列一些名称,这些名称都为常量,并且每个常量会记住枚举类的一个对象
public static final A Y=new A();//如果只有一个Y的话,可以被视为单例类。
public static final A Z=new A();
//枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
public static A[] values();//返回枚举类的所有实例
public static A valueOf(java.lang.String var0);//返回指定名称的枚举实例
}
- 枚举类的常用应用场景:适合做信息分类和标志
(1)枚举类常用于定义常量,枚举类中的常量只能是枚举类的实例,不能被修改。
(2)虽说用也可以用常量类做信息分类和标志,但是参数值不受约束(就是传入的参数不是限定内的那四个都能行的通,写别的不报错),但是枚举类的话写限定外的就会报错。
七)抽象类
- 在java中有一个关键字叫:abstract(抽象),可以用它修饰类、成员方法。
- abstract修饰类,这个类就是抽象类。
- abstract修饰方法,这个方法就是抽象方法。
修饰符 abstract class 类名{
修饰符 abstract 返回值类型 方法名称(形参列表);
}
抽象方法只有方法签名,不能写方法体。
- 抽象类的注意事项、特点
(1)抽象类不一定要有抽象方法,有抽象方法的类必须是抽象类。
(2)类有的成员:成员变量、方法、构造器,抽象类都可以有。
(3)抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
(4)一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义为抽象类。
点击查看代码
public abstract class A {//抽象的本质是不能有对象,抽象类的基本作用就是被继承
private String name;//抽象类有成员变量
public A(){System.out.println("抽象类A的构造方法");}//抽象类有构造方法
public void show(){System.out.println("抽象类A的show方法");}//抽象类的方法可以有方法体
//抽象方法,抽象类中可以有没有抽象方法,抽象方法没有方法体,只有方法声明,子类必须重写抽象方法
public abstract void show();//抽象类的抽象方法不能写方法体
}
- 抽象类的应用场景和好处:父类知道每个子类都要做某个行为,但每个子类要做的情况不一样,父类就定义成抽象方法,交给子类去重写实现,我们抽出这样的抽象类,就是为了更好的支持多态。(抽象类简化了需要重写的父类方法的代码,更加便于调用子类时多态的实现,更具解耦性。)
- 模板方法设计模式:
(1)提供了方法作为完成某类操作的模板,模板方法封装了每个实现步骤,允许子类实现特定步骤的实现。
(2)解决方法中存在重复代码的问题。
(3)定义一个抽象类,在里面定义2个方法,一个是模板方法:放相同的代码,一个是抽象方法:具体实现交给子类完成。
(4)建议使用final关键字修饰模板方法。(模板方法不能被子类重写,一旦子类重写了模板方法,模板方法就失效了)
点击查看代码
public abstract class fu {//父类抽象类
public final void show(){//模板方法不能被重写
System.out.println("模板方法");//子类共同的部分
show1();//子类不同的部分
}
public abstract void show1();//抽象方法,子类一定要重写,不然就报错,这样做是最佳实现
}
public class A extends fu {//子类
@Override
public void show1(){System.out.println("A show1方法");}
}
public class B extends fu {//子类
@Override
public void show1(){System.out.println("B show1方法");}
}
八)接口(相当于干爹)
- Java提供了一个关键字interface定义出接口。
public interface 接口名{
//成员变量(常量)
//成员方法(抽象方法)
}
(1)传统接口:内部只能写常量和抽象方法(jdk8以前)。
(2)常量:接口中的常量默认是public static final的,所以可以省略。
(3)抽象方法:接口中的抽象方法默认是public abstract的,所以可以省略。
(4)没有构造方法。
- 注意:接口不能创建对象。
- 接口是用来被类实现(implements)的,实现接口的类称为实现类,一个类可以同时实现多个接口。
修饰符 class 实现类类名 implements 接口1,接口2,接口3,···{
//实现类实现多个接口,必须重写完全部接口的全部抽象方法,否则实现类需要定义成抽象类。
}
点击查看代码
public interface A {//接口不能创建对象,只能被实现
//JDK-8之前,接口只能定义常量和抽象方法,不能定义具体方法。
//1、常量:接口中的常量默认是public static final的,所以可以省略。
String NAME="rasion";
//2、抽象方法:接口中的抽象方法默认是public abstract的,所以可以省略。
// public abstract void show();
void show();
}
public class C implements A,B {//实现接口的类,可以实现多个接口
@Override
public void show() {
System.out.println("C实现A\B接口的show方法");
}
}
- 接口的好处:
(1)弥补了类单继承的不足,可以实现多接口,使类的角色更多,功能更加强大(接口可以是一种属性)。
(2)让程序可以面向接口编程,这样程序员就可以灵活方便的切换各种业务实现(更利于程序的解耦合)。
点击查看代码
public class test {
public static void main(){
// Driver d=new Teacher();//做接口解耦合,可以不改变自己本来的代码
Driver d=new Student();
Cook c=new Student();
}
}
public interface Driver {}
public interface Cook {}
class people{}//抽象类
class Teacher extends people implements Driver,Cook{}//实现多个接口,用逗号隔开
class Student extends people implements Driver,Cook{}
- JDK8开始,接口新增了三种形式的方法:
(1)默认方法:使用default修饰,使用实现类的对象调用。
(2)静态方法:static修饰,必须当前接口名来调用。
(3)私有方法:private修饰,jdk9开始才有的,只能在接口内部被调用。
(4)他们都会默认被public修饰。
(5)新增的方法增强接口能力,更便于项目扩展与维护(若新增方法,可以直接写在接口中,不用修改实现类,更方便扩展)
点击查看代码
//jdk-8之后的三种方法
//a、默认方法,一定要加default修饰符,默认会用public修饰,用接口的实现类对象调用
default void defaultMethod(){
System.out.println("defaultMethod");
privateMethod();
}
//b、静态方法,一定要加static修饰符,默认会用public修饰,只能用当前接口名来调用
static void staticMethod(){//静态方法
System.out.println("staticMethod");
}
//c、私有方法,一定要加private修饰符,默认会用private修饰,用接口中的其他实例方法调用
private void privateMethod(){//私有方法
System.out.println("privateMethod");
}
- 注意事项:
(1)接口与接口可以多继承,一个接口可以同时继承多个接口,但接口不能继承类。
类与类:单继承,一个类只能继承一个直接父类。
类与接口:多实现,一个类可以同时实现多个接口
(2)一个接口继承多个接口,若多个接口中出现方法签名冲突,此时不支持多继承,也不支持多实现(即两个接口都有同一个方法名,但是方法签名不同,即方法类型不同,则报错)。
(3)一个类继承了父类,同时又实现接口,若父类与接口中有同名的方法,则会优先实现父类的方法。(若想调用接口的方法只能新建中专方法,接口名.super.方法名)
(4)一个类实现多个接口,如果多个接口中存在同名的默认方法,要想不冲突,这个类重写该方法即可。
- 抽象类与接口区别:
相同点:
(1)都是抽象形式,都可以有抽象方法,都不能创建对。
(2)都是派生子类形式:抽象类是被子类继承使用,接口是被实现类实现。
(3)一个类继承抽象类,或者实现接口,都必须重写他们的抽象方法,否则自己要成为抽象类或者报错!
(4)都能支持多态,都能实现解耦合。
不同点:
(1)抽象类中可以定义类的全部普通成员,接口只能定义常量、抽象方法,(JDK8新增的三种方式)。
(2)抽象类只能被类单继承,但是接口可以被类多实现。
(3)一个类继承抽象类就不能再继承其他类,一个类实现了接口(还可以继承其他类或者实现其他接口)。
(4)抽象类体现模板思想,更利于做父类的,实现代码的复用性;接口更适合做功能的解耦合,解耦合更加灵活。(最佳实现)
九)代码块
- 类的五大成分:成员变量、构造器、方法、代码块、内部类。
- 静态代码块:
(1)格式:static{ }
(2)特点:类加载时自动执行,由于类只会加载一次,所以静态代码块只执行一次,优先考虑于构造器执行。
(3)作用:完成类的初始化(如:对静态变量的初始化赋值)。
(4)import java.util.Arrays:
System.out.println(Arrays.toString(cards));//返回数组的内容观察
- 实例代码块
(1)格式:{ }
(2)特点:每次创建对象时,执行实例代码块,并在构造器前执行。
(2)作用:和构造器一样,都是用来完成对象的初始化的(如:对实例变量进行初始化赋值)。
点击查看代码
public class codeBlock {
public static String NAME;
public static String[] CARD=new String[54];//初始化数组
static {//静态代码块,有static修饰,属于类,与类一起优先加载,自动执行一次
System.out.println("static block");
NAME="rasion";
CARD[0]="A";//静态数组只需要初始化一次,就可以在静态代码块中初始化
CARD[1]="2";
CARD[2]="3";
}
{//实例代码块,没有static修饰,属于对象,与对象一起加载,对象加载几次,就会执行几次
System.out.println("non-static block");
}
public static void main(String[] args) {
System.out.println("hello main");
System.out.println(NAME);
System.out.println(Arrays.toString(CARD));//打印数组
codeBlock cb=new codeBlock();//创建对象时,会执行实例代码块
new codeBlock();
new codeBlock();
System.out.println(cb);
}
}
十)内部类
- 当一个类被定义在另一个类内部,则该类称为内部类。
- 场景:当一个类的内部,包含了一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。
- 成员内部类:
(1)就是类中的一个普通成员,类似前面我们学过的普通的成员变量、成员方法。
(2)创建成员内部类对象格式:
外部类名.内部类名 对象名= new 外部类名().new 内部类名();
Outer.Inter in=new Outer().new Inter();
(3)成员内部类属于外部类对象持有。
(4)成员内部类可以直接访问外部类的静态成员、实例成员。
(5)成员内部类的实例方法中,可以直接拿到当前寄生的外部类对象,格式是:外部类.this.成员名。
点击查看代码
public class main {
public static void main(String[] args) {
// 创建成员内部类对象格式:外部类名.内部类名 对象名= new 外部类名().new 内部类名();
OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
innerClass.print();//内部类对象调用内部类方法
}
}
public class OuterClass {
//成员内部类:无static修饰,属于外部类的对象持有的
public class InnerClass{//类有的属性,方法内部类都能有
private String num="InnerThis";
public void print(){
System.out.println("===innerMethod===");
outerPrint();
String num="-innerPrint-";
System.out.println(num);
System.out.println(this.num);//内部类对象
System.out.println(OuterClass.this.num);//外部类对象
OuterClass oc=new OuterClass();//创建外部类对象
System.out.println(oc.num);
run();
}
}
//内部类可以访问外部类的静态成员
public static void outerPrint(){//外部类静态方法
System.out.println("OuterClass static print");
}//静态内部类,属于外部类,外部类可以访问
private String num="OuterThis";//外部类成员变量
public void run(){}
}
- 静态内部类:
(1)有static修饰的内部类,属于外部类自己持有。
(2)创建静态内部类对象格式:
外部类名.内部类名 对象名= new 外部类名.内部类名();
Outer.Inner in=new Outer.Inner();
(3)静态内部类可以直接访问外部类的静态成员;静态内部类不可以直接访问外部类的实例成员。
点击查看代码
public class main {
public static void main(String[] args) {
StaticInnerClass.InnerClass innerClass=new StaticInnerClass.InnerClass();//静态内部类对象
innerClass.print();
}
}
public class StaticInnerClass {
public static class InnerClass{
private String NAME="inner rasion";
public void print() {
System.out.println("StaticInnerClass print");
//静态内部类可以直接访问外部类的静态成员
System.out.println("StaticInnerClass NAME:"+StaticInnerClass.NAME);
//静态内部类不可以直接访问外部类的实例成员
// System.out.println("StaticInnerClass age:"+age);
//静态内部类可以间接访问外部类的局部变量
StaticInnerClass staticInnerClass=new StaticInnerClass();
System.out.println("StaticInnerClass age:"+staticInnerClass.age);
}
}
public static String NAME="outer rasion";//外部类的静态成员
public int age=18;//外部类的实例成员,属于外部类对象的
}
- 局部内部类:(了解)
局部内部类:定义在方法中、代码块中、构造器等执行体中(无具体意义,用来引出匿名内部类)。
- 匿名内部类:
(1)一种特殊的局部内部类。
(2)所谓匿名:指的是程序员不需要为这个类声明名字,默认有一个隐藏的名字。
new 类名或接口(参数值...){
类体(一般是方法重写);
};
new Animal(){
@Override
public void cry(){}
};
(3)特点:匿名内部类本质是个子类,并且会立即创建出一个子类对象。
(4)作用:更方便的创建一个子类对象。
(5)匿名内部类在开发中的常见形式:通常作为一个对象参数传输给方法。
(6)匿名内部类在开发中的真实使用场景示例
调用别人提供的方法实现需求时,这个方法正好可以让我们传输一个匿名内部类对象给其使用。(开发中不是主动写匿名内部类,而是调用别人的功能时,需要我们写一个匿名内部类)
点击查看代码
public class AnonymityInnerClass {
public static void main(String[] args) {
//匿名内部类有名字:外部类名$编号.class
//匿名内部类本质是一个子类,同时立即创建一个子类对象
Animal cat=new Animal(){
@Override
public void run() {System.out.println("cat run");}
};//匿名内部类,同cat类,即是一个子类又是一个子类对象
// Animal cat=() -> System.out.println("cat run");//Lambda简化
cat.run();
print(cat);
print(new Animal() {
@Override
public void run() {System.out.println("inner cat run");}
});//同上print(cat);一样功能
}
public static void print(Animal animal){
System.out.println("start==");
animal.run();//匿名类对象回调,由于匿名类相当于子类,所以可以调用子类的方法
}
}
interface Animal{
public abstract void run();
}
//class cat extends Animal{
// @Override
// public void run() {
// System.out.println("cat run");
// }
//}
十一)函数式编程
- 此“函数”类似于数学中的函数(强调做什么),只要输入的数据一致返回的结果也是一致的
数学中的函数示例:2x+1
Java中的函数(Lambda表达式):(x->2x+1)
- 使用Lambda函数替代某些匿名内部类对象,从而让程序代码更简洁,可读性更好。
- Lambda表达式
(1)JDK8新增的一种语法,代表函数。
(2)可以用于替代并简化函数式接口的匿名内部类,从而让程序更简洁,可读性更好。
Lambda表达式的格式:(被重写方法的形式参数列表)->{匿名内部类被重写的方法体代码}
(3)注意:Lambda表达式只能替代函数式接口的匿名内部类!!!
(4)函数式接口:有且仅有一个抽象方法的接口。
(5)注意:将来我们见到的大部分函数式接口,上面都可能会有一个@FunctionalInterface的注解,该注解用于约束当前接口必须是函数式接口。
(6)Lambda表达式的省略规则(用于进一步简化Lambda表达式的写法):
a、参数类型可以省略不写。
b、如果只有一个参数,参数类型省略的同时“( )”可以省略,但多个参数不能省略“( )”
c、如果Lambda只有一行代码,大括号可以省略,同时必须省略分号“;”,如果这行代码是return语句,也必须去掉return。
点击查看代码
public class main {
public static void main(String[] args) {
//函数式接口:
MyInterface myInterface1 = () -> {
System.out.println("lambda表达式简化匿名内部类");
};
myInterface1.print();
//public static void sort(T[] a, Comparator<T> c)——————作用:数组按照某个值重排序
//参数一:需要排序的数组 参数二:需要给sort声明一个Comparator比较器对象(指定排序的规则)
Arrays.sort(students,new Comparator<Student>() {//Comparator为由官方定义的函数式接口
@Override
public int compare(Student o1, Student o2) {
//如果左边对象大于右边对象,返回正整数,否则返回负整数,等于时为0
return o1.getAge()-o2.getAge();//或写成这样,按照年龄升序
}
});
Arrays.sort(students, (o1, o2)-> o1.getAge()-o2.getAge());//以上部分简化后的代码
}
}
//函数式接口:只有一个抽象方法的接口
@FunctionalInterface//标记函数式接口,编译器会检查,如果接口中有多个抽象方法,编译器会报错
interface MyInterface{
//抽象方法
public abstract void print();
}
- 方法引用:
- 静态方法引用
(1)格式:类名::静态方法名
(2)使用场景:如果某个Lambda表达式里仅调用了一个静态方法,并且"->"前后的参数形式一致,则可以使用静态方法引用。
点击查看代码
// Arrays.sort(students, (o1, o2)-> o1.getAge()-o2.getAge());//Student没有加compareByAge方法时
Arrays.sort(students,(o1, o2)->Student.compareByAge(o1,o2) );//Student加compareByAge方法时可化简,Lambda表达式实现
Arrays.sort(students, Student::compareAge);//静态方法引用简化后的代码
public class Student{
...(详见Student类)
public static int compareByAge(Student o1, Student o2){
return o1.getAge()-o2.getAge();
}
}
- 实例方法引用
(1)格式:对象名::实例方法名
(2)使用场景:如果某个Lambda表达式里只通过对象名称调用一个实例方法,并且"->"前后的参数形式一致,则可以使用实例方法引用。
点击查看代码
Student t=new Student();//创建一个实例对象
Arrays.sort(students, (s1,s2) -> t.compareByHeight(s1,s2));//Lambda使用方式
Arrays.sort(students, t::compareByHeight);//实例方法引用
public class Student{
...(详见Student类)
public int compareByHeight(Student o1, Student o2){
//按身高升序排序
return Double.compare(o1.getHeight(),o2.getHeight());
}
}
- 特定类的方法引用
(1)格式:特定类型名称(如:String)::方法名
(2)使用场景:如果某个Lambda表达式里只调用一个特定类型的实例方法,并且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为实例方法的入参的,则此时就可以使用特定类型的方法引用。
点击查看代码
Arrays.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);//字符串按照首字母忽略大小写比较的方法
}
});//匿名内部类的代码
Arrays.sort(names,(o1, o2)-> o1.compareToIgnoreCase(o2));//Lambda简化后的代码
Arrays.sort(names,String::compareToIgnoreCase);//特定类的方法引用
- 构造器引用
(1)格式:类名::new
(2)使用场景:如果某个Lambda表达式里只是创建对象,并且"->"前后的参数形式一致,则可以使用构造器引用。
点击查看代码
package com.itheima.method1reference;
import jdk.jfr.DataAmount;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
public class Demo4 {
public static void main(String[] args) {
// CarFactory cf = new CarFactory() {
// @Override
// public Car getCar(String name) {
// return new Car(name);
// }
// };//匿名内部类
// CarFactory cf= name -> new Car(name);//Lambda代码
//以上两种代码简化后的代码:
CarFactory cf= Car::new;//构造器引用
Car c1 = cf.getCar("奔驰");
System.out.println(c1);
}
}
@FunctionalInterface
interface CarFactory{
Car getCar(String name);
}
@Data//封装数据
@AllArgsConstructor//有参构造器
@NoArgsConstructor
class Car{
private String name;
}
十二)常用API(应用程序编程接口)
简单理解:API就是别人写好的东西,我们不需要自己编写,直接使用即可。
Java API:指的就是JDK中提供的各种功能的Java类。
- String(代表字符串,它的对象可以封装字符串数据,并提供了很多方法完成对字符串的处理)
- String创建字符串对象的方式
(1)方式一(推荐):直接“”就可以创建字符串对象,封装字符串数据(java程序中的所有字符串文字(例如”abc“)都为此
String name="小黑";
String schoolName="黑马程序员";
(2)方式二:调用String类的构造器初始化字符串对象。
(3)只要以”...“方式创建字符串对象,会存储到字符串常量池中,且相同字符串只会储存一份。
(4)通过 new 方式创建字符串对象,每new一次,都会产生一个新的对象放在堆内存中。
(5)字符串对象的内容比较,千万不要用,默认比较地址,字符串对象的内容一样时地址不一定一样。
点击查看代码
//方式一(推荐):直接“”就可以创建字符串对象,封装字符串数据
String s1="hello,黑马";//快速创建字符串
System.out.println(s1);//输出hello,黑马而不是地址
//方式二:通过构造器初始化对象。
String s2=new String();//不推荐
System.out.println(s2);//”“空字符串
String s3=new String("hello,黑马");//不推荐
char[] chs={'h','e','l','l',',','黑','马'};
String s4=new String(chars);//把字符数组转换成字符串
byte[] bytes={97,98,99,65,66,67};
String s5=new String(bytes);
System.out.println(s5)
String t1="abc";
String t2="abc";
System.out.println(t1=t2);//字符串在常量池且只有一份,故为true
String t3=new String("abc");
String t4=new String("abc");
System.out.println(s3==s4);//new出字符串对象,故为false
| 构造器 |
说明 |
| public String() |
创建一个空白字符串对象,不含任何内容 |
| public String(String original) |
根据传入的字符串内容,来创建字符串对象 |
| public String(char[] chars) |
根据字符数组的内容,来创建字符串对象 |
| public String(byte[] bytes) |
根据字节数组的内容,来创建字符串对象 |
| 方法名 |
说明 |
| public int length() |
获取字符串长度返回(字符个数) |
| public char charAt(int index) |
获取某个索引位置处的字符返回 |
| public char[ ] toCharArray() |
讲当前字符串转换成字符数组返回 |
| public boolean equals(Object anObject) |
判断当前字符串与另一个字符串的内容是否一样,一样返回true |
| public boolean equalsIgnoreCase(String anotherString) |
判断当前字符串与另一个字符串的内容是否一样(忽略大小写) |
| public String substring(int beginIndex,int endIndex) |
根据开始和结束索引进行截取,得到新的字符串(包前不包后) |
| public String substring(int beginIndex) |
从传入的索引处截取,截取到末尾,得到新的字符串返回 |
| public String replace(CharSequence target,CharSequence replacement) |
使用新值,讲字符串中的旧值替换,得到新的字符串 |
| public boolean containt(CharSequence s) |
判断字符串中是否包含了某个字符串 |
| public boolean startsWith(String prefix) |
判断字符串是否以某个字符串内容为开头,是则返回true |
| public String[ ] split(String regex) |
把字符串按照某个字符串内容分割,返回字符串数组回来 |
- ArrayList
(1)集合是一种容器,用来装数据的,类似数组,但是容量大小可变,功能丰富,开发中用的更多。
(2)ArrayList是集合中最常用的一种,ArrayList是泛型类,可以约束存储的数据类型。
ArrayList<E> list=new ArrayList<>();
(3)创建ArrayList对象,代表一个集合容器(调用无参构造器public ArrayList()初始化对象):ArrayList list=new ArrayList<>();
(4)创建ArrayList对象时,如果没有用泛型,则调用get方法时返回Object类型的数据。
(4)调用ArrayList提供的方法,对容器中的数据进行增删改查操作。
(5)遍历集合:for(int i=0;i<list.size();i++){String s=list.get(i);System.out.println(s);}
| 常用方法 |
说明 |
| public ArrayList() |
创建一个空的集合对象 |
| public boolean add(E e) |
在集合的末尾添加指定的元素,并返回true |
| public void add(int index,E element) |
在集合的指定索引位置添加指定的元素 |
| public E get(int index) |
返回指定索引位置的元素 |
| public int size() |
返回集合中元素的个数 |
| public E remove(int index) |
删除指定索引位置的元素,并返回被删除的元素 |
| public boolean remove(Object o) |
删除指定元素,并返回删除是否成功 |
| public E set(int index,E element) |
修改指定索引位置的元素,并返回被修改的元素 |
十三)GUI编程(非必学)
- 什么是GUI
(1)GUI,全称Graphical User Interface,指图形用户界面。
(2)通过图形元素(如窗口、按钮、文本框等)与用户进行交互。
(3)与命令界面(CLI)相比,GUI更加直观、友好。
- Java的GUI编程包
(1)AWT:提供了一组原生的GUI组件,依赖于操作系统的本地窗口系统。(几乎不用)
(2)Swing:基于AWT包,提供了更丰富的GUI组件,轻量级组件,不依赖于本地窗口系统。
(3)常见的Swing组件:JFrame:窗口、JPanel:用于组织其他组件的容器、JButton:按钮组件、JTextField:输入框、JTable:表格、...
点击查看代码
private static void createWindow() {//以下为创建窗口和一个按钮的代码
JFrame frame = new JFrame("登录窗口");//创建窗口
JPanel panel = new JPanel();//创建面板
frame.add(panel);//将面板添加到窗口中
frame.setSize(300, 200);//设置窗口大小
frame.setLocationRelativeTo(null);//设置窗口居中
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置关闭窗口默认操作:关闭窗口,推出程序
JButton button=new JButton("登录");//创建按钮
//button.setBounds(100,100,80,30);//设置按钮位置和宽高
panel.add(button);//添加按钮到面板,使按钮能够自适应窗口大小
frame.setVisible(true);//显示窗口
}
- 常见的布局管理器
- 布局管理器(是Java Swing中用于控制容器中组件排列和大小的对象)可以决定组件在容器中的布局方式,避免了手动设置每个组件的位置和大小,从而简化了GUI设计过程。
- 常见的布局管理器(FlowLayout、BorderLayout、GridLayout、BoxLayout)。
- FlowLayout:按照水平方向从左到右排列组件,当一行排满时,自动切换到下一行
特点:默认居中对齐,可以设置左对齐或右对齐;适用于需要简单排列的场景。
FlowLayout示例代码
JFrame frame = new JFrame("FlowLayout布局管理器");
frame.setSize(400, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new FlowLayout());//设置窗口的布局管理器对象
frame.add(new JButton("按钮1"));
frame.add(new JButton("按钮2"));
frame.setVisible(true);
- BorderLayout:将容器分为5个区域,East、South、West、North、Center(东、南、西、北、中),每个区域只能添加一个组件,未添加组件的区域保持空白。
特点:适用于要在特定区域布局组件的场景;中间的组件会自动填充剩余的空间。
BorderLayout示例代码
JFrame frame = new JFrame("BorderLayout布局管理器");
frame.setSize(400, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());//设置窗口的布局管理器对象
frame.add(new JButton("按钮1"), BorderLayout.NORTH);
frame.add(new JButton("按钮2"), BorderLayout.SOUTH);
frame.add(new JButton("按钮3"), BorderLayout.CENTER);
frame.add(new JButton("按钮4"), BorderLayout.EAST);
frame.add(new JButton("按钮5"), BorderLayout.WEST);
frame.setVisible(true);
- GridLayout:将容器按照指定的行数和列数划分为等大小的网格,每个组件占一个单元格,所有组件大小相同。
特点:适用于需要均匀排列组件的场景;行和列的数量可以指定。
GridLayout示例代码
JFrame frame = new JFrame("GridLayout布局管理器");
frame.setSize(400, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new GridLayout(2, 3));//设置窗口的布局管理器对象
for (int i = 0; i < 6; i++) {
frame.add(new JButton("按钮" + i));
}
frame.setVisible(true);
- BoxLayout:将容器按照单一水平或垂直方向排列组件,可以创建水平(X轴)或垂直(Y轴)排列的布局。
特点:适用于需要沿单一方向排列组件的场景;可以添加垂直或水平间隔(Glue、Strut)来调整组件间距。
BoxLayout示例代码
JFrame frame = new JFrame("BoxLayout布局管理器");
frame.setSize(400, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));//垂直排列
panel.add(new JButton("按钮1"));
panel.add(Box.createVerticalStrut(10));//垂直间距
panel.add(new JButton("按钮2"));
panel.add(Box.createVerticalStrut(10));
panel.add(new JButton("按钮3"));
frame.add(panel);
frame.setVisible(true);
- 事件处理:在GUI编程中,Java通过事件监听器(EventListener)来完成。
- 常见的事件监听器对象:点击事件监听器(ActionListener)、按键事件监听器(KeyListener)、鼠标行为监听器(MouseListener)、...
点击事件监听器(ActionListener)示例代码
JFrame jf=new JFrame("登录窗口");
JPanel panel=new JPanel();//创建一个面板
jf.add(panel);//将面板添加到窗口中
jf.setSize(400,300);//设置窗口大小
jf.setLocationRelativeTo(null);//设置窗口居中
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置关闭窗口的默认操作:关闭窗口退出程序
JButton jb=new JButton("登录");//创建一个按钮
panel.add(jb);//将按钮添加到面板中
//给按钮绑定点击事件监听对象。
jb.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
//一旦你点击jb按钮,底层触发这个方法执行
//e 是事件对象,封装了事件相关信息
JOptionPane.showMessageDialog(jf,"有人点击了登录");
}
});
jf.setVisible(true);//显示窗口
按键事件监听器(KeyListener)示例代码
JFrame jf=new JFrame("登录窗口");
JPanel panel=new JPanel();//创建一个面板
jf.add(panel);//将面板添加到窗口中
jf.setSize(400,300);//设置窗口大小
jf.setLocationRelativeTo(null);//设置窗口居中
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置关闭窗口的默认操作:关闭窗口退出程序
JButton jb=new JButton("登录");//创建一个按钮
panel.add(jb);//将按钮添加到面板中
//需求:监听用户键盘上下左右四个按键的事件。
//给jf窗口整体绑定按键事件。
jf.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e){
System.out.println("========AAAAAAA========");
//获取键盘按键的编码
int keyCode=e.getKeyCode();//拿事件源头的键帽编号
//判断按键编码是否是上、下、左、右
if(keyCode==KeyEvent.VK_UP){
System.out.println("用户点击了上");
}else if(keyCode== KeyEvent.VK_DOWN){
System.out.println("用户点击了下");
}else if(keyCode== KeyEvent.VK_LEFT) {
System.out.println("用户点击了左");
}else if(keyCode== KeyEvent.VK_RIGHT) {
System.out.println("用户点击了右");
}
}
});
jf.setVisible(true);//显示窗口
//让窗口成为焦点
jf.requestFocus();
- 事件的几种常见写法:
点击查看代码
package com.itheima.gui;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Test2 {
public static void main(String[] args) {
JFrame jf = new JFrame("登录窗口");
JPanel panel = new JPanel();//创建一个面板
jf.add(panel);//将面板添加到窗口中
jf.setSize(400, 300);//设置窗口大小
jf.setLocationRelativeTo(null);//设置窗口居中
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置关闭窗口的默认操作:关闭窗口退出程序
JButton jb = new JButton("登录");//创建一个按钮
panel.add(jb);//将按钮添加到面板中
jb.addActionListener(new MyActionListener(jf));
jf.setVisible(true);
}
}
//实现类
class MyActionListener implements ActionListener{
private JFrame jf;
public MyActionListener(JFrame jf){
this.jf=jf;
}
@Override
public void actionPerformed(ActionEvent e){
JOptionPane.showMessageDialog(jf,"有人点击了登录!");
}
}
点击查看代码
JFrame jf=new JFrame("登录窗口");
JPanel panel=new JPanel();//创建一个面板
jf.add(panel);//将面板添加到窗口中
jf.setSize(400,300);//设置窗口大小
jf.setLocationRelativeTo(null);//设置窗口居中
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置关闭窗口的默认操作:关闭窗口退出程序
JButton jb=new JButton("登录");//创建一个按钮
panel.add(jb);//将按钮添加到面板中
//给按钮绑定点击事件监听对象。
jb.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e){
//一旦你点击jb按钮,底层触发这个方法执行
//e 是事件对象,封装了事件相关信息
JOptionPane.showMessageDialog(jf,"有人点击了登录");
}
});
jf.setVisible(true);//显示窗口
- 自定义窗口,让窗口对象实现事件接口,重写监听器接口方法。即:
public class LoginFrame extends JFrame implements ActionListener{}
点击查看代码
public class Test3 {
public static void main(String[] args){
LoginFrame lf=new LoginFrame();
lf.setVisible(true);//显示窗口
}
}
}
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
//自定义的登陆界面:认JFrame做爸爸。
public class LoginFrame extends JFrame implements ActionListener {
public LoginFrame(){
//1.设置窗口的标题
this.setTitle("登录界面");
//2.设置窗口的位置
this.setSize(400,300);
//3.设置窗口的关闭方式
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
init();//初始化这个窗口上的组件
}
private void init(){
//添加一个登录按钮
JButton btn=new JButton("登录");
btn.addActionListener(this);//添加监听器
JPanel panel=new JPanel();
this.add(panel);
panel.add(btn);
}
@Override
public void actionPerformed(ActionEvent e){
JOptionPane.showMessageDialog(this,"我被点击了~");
}
}
- GUI项目练习笔记
(1)石头迷阵的游戏
(2)黑马人事管理系统
二、JavaSE基础加强
(一)Java异常
Ⅰ.认识异常
- 异常是代码在编译或者执行的过程中可能出现的错误。
- Java异常体系:Java.lang.Throwable之下的异常类。
Throwable(父类)-->Error/ (Excetion-->RuntimeException/其他异常)
(1)Error:代表的系统级别错误(属于严重问题),也就是说系统一旦出问题,sun公司会把这些异常封装成Error对象抛出(Error是给sun公司自己用的,不是给程序员用的,因此我们开发人员不用管他),如:内存溢出、栈溢出等。
(2)Exception:异常,它代表的才是程序运行过程中出现的问题,程序员通常会用Exception以及它的孩子来封装程序出现的问题。(又分为运行时异常和编译时异常)
-- 运行时异常:RuntimeException及其子类,编译阶段不会出现错误提醒,运行时出现异常(如:数组索引越界异常)。
-- 编译时异常:编译阶段就会出现错误提醒。(如:日期解析异常、语法错误、类型转换错误等)
- 异常的基本处理
(1)抛出异常(throws):在方法上使用throws关键字,可以将方法内部出现的异常抛出去给调用者处理。
(2)捕获异常(try...cath):直接捕获程序出现的异常。
方法() throws 异常1,异常2,异常3...{
...
}//或遇到多个异常可以只抛出父异常类型Exception。
try{
//监视可能出现异常的代码!
}catch(异常类型1 变量){
//处理异常
}catch(异常类型2 变量){
//处理异常
}...
点击查看代码
public class LearnException{
public static void main(String[] args) {
// Runtimeshow();
// CompileShow();//方法一、在main添加throws ParseException标签抛出异常
try {//方法二、捕获异常
CompileShow();
} catch (ParseException e){
e.printStackTrace();//输出异常信息
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
try {//两个异常时可以只抛一个Exception类异常,同时CompileShow只需捕获Exception异常即可
CompileShow();
} catch (java.lang.Exception e){
e.printStackTrace();
}
}
public static void Runtimeshow(){//运行时异常
System.out.println("程序开始");
//运行时异常:编译阶段不报错,运行时出现的异常,继承自 RuntimeException
int[] arr = new int[]{1,2,3};
System.out.println(arr[3]);//java.lang.ArrayIndexOutOfBoundsException——数组索引越界
System.out.println(10/0);//java.lang.ArithmeticException——除数0,数字操作异常
System.out.println(String.valueOf(null));//java.lang.NullPointerException——空指针异常
System.out.println("程序结束");
}
public static void CompileShow() throws ParseException, FileNotFoundException {//两个异常一起抛可以只抛一个Exception父异常
System.out.println("程序开始");
//编译时异常:编译阶段报错,编译不通过,继承自 Exception
String str = "2024-08-09 11:40:00";
//把字符串时间解析成Java中的一个日期对象。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = sdf.parse(str);//编译时异常,提醒这里代码容易出错
System.out.println(date);
InputStream is = new FileInputStream("D/tupian.png");//在D盘中读取tupian.png
System.out.println("程序结束");
}
}
Ⅱ.异常作用
- 用来定位程序bug关键信息
- 可以作为方法内的一种特殊的返回值,以便通知上层调用者,方法的执行出现问题,用来查找bug。
点击查看代码
public class ExceptionAffect {
public static void main(String[] args) {
try{
System.out.println(div(10,0));
System.out.println("方法执行成功");
}catch(Exception e){//抛出异常能让程序知道下一步要执行什么
e.printStackTrace();
System.out.println("程序出现异常,程序退出");
}//执行完异常程序也不会死亡
}
public static int div(int a,int b) throws Exception{
if(b==0){
System.out.println("除数不能为0");
throw new Exception("除数不能为0,参数有问题");//可以替代return -1;而且更具安全性
}
int result=a/b;
return result;
}
}
Ⅲ.自定义异常
- Java无法为全部的问题都提供异常类来代表,企业内部某种问题想通过异常管理,以便用异常来管理问题,需要自定义异常类。
- 自定义编译异常:
(1)定义一个异常类继承Exception类。
(2)并重写构造器。
(3)通过throw new 异常类(xxx) 创建异常对象并抛出。
特点:编译阶段就报错,提醒比较激进。
- 自定义运行时异常:
(1)定义一个异常类继承RuntimeException类。
(2)并重写构造器。
(3)通过throw new 异常类(xxx) 创建异常对象并抛出。
特点:编译阶段不报错,运行时才可能出现!提醒不属于激进型。
- 如果想要表达强烈的提醒(他人易犯的错误),就要用编译时异常(少用),若然程序员自己能避免(别人不易犯错),就使用运行时异常(多用)。
点击查看代码
public class SelfException {
//运行时异常和编译时异常代码基本一致,只有名称有些差别:AgeIllegalRuntimeException和AgeIllegalException
public static void main(String[] args) {
try {checkAge(15);
} catch (AgeIllegalRuntimeException e) {
e.printStackTrace();
System.out.println("运行异常:程序出现异常,程序退出");
}
}
public static void checkAge2(int age) throws AgeIllegalRuntimeException{
if(age<18||age>200){
throw new AgeIllegalRuntimeException("运行异常:年龄非法,年龄不能小于18,不能大于200");
}else{System.out.println("年龄合法");}
}
}
//自定义编译时异常
class AgeIllegalException extends Exception{
public AgeIllegalException(){}
public AgeIllegalException(String message){
super(message);
}
}
//自定义运行时异常
class AgeIllegalRuntimeException extends RuntimeException{
public AgeIllegalRuntimeException(){}
public AgeIllegalRuntimeException(String message){
super(message);
}
}
Ⅳ.异常处理方案
- 底层异常层层上抛,最外层捕获异常,记录下异常信息,并响应适合用户观看的信息进行提醒。
- 最外层获取异常后,尝试重新修复。
点击查看代码
public class ExceptionDeal {//最外层获取异常后,尝试重新修复。
public static void main(String[] args) throws Exception {
while(true){
try {
int age=checkAge();
System.out.println("年龄:"+age);
break;
}catch (Exception e){
// e.printStackTrace();//用来报错
System.out.println("请重新输入:");
}
}
}
public static int checkAge( ) throws Exception{
Scanner sc=new Scanner(System.in);
System.out.println("请你输入年龄:");
int age=sc.nextInt();
if(age<18||age>200){
throw new Exception("年龄非法,年龄不能小于18,不能大于200");
}else{
return age;
}
}
}
(二)泛型
Ⅰ.认识泛型
- 定义类、方法、接口时,同时声明了一个或多个类型变量(如:< E >)称为泛型类、泛型接口、泛型方法,它们统称为泛型。
public class ArrayList<E>{
...
}
- 作用:提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力,可以避免强制类型转换,及其有可能出现的异常。
- 泛型本质:把具体的数据类型作为参数传给类型变量。
Ⅱ.泛型类
- 基本语法:
修饰符 class 类名<类型变量1、类型变量2,...>{ }
- 注意:类型变量用大写字母,如:T、E、K、V等。
- 可以控制类接收的类型变量,由于支持多个类型变量,故需注意类型变量的顺序,如:<T,E>、<E,T>之类的。
- 应用场景:在工具类中,经常会有一些方法需要处理不同类型的对象,如集合操作、数据转换等,这时可以使用泛型方法来增强工具类的通用性。
Ⅲ.泛型接口
- 基本语法:
修饰符 interface 接口名<类型变量1、类型变量2,...>{ }
- 注意:类型变量用大写字母,如:T、E、K、V等。
点击查看代码
public class GenericDemo3 {
public static void main(String[] args){
//需求:项目需要对学生数据/老师数据都要进行增删改查操作
StudentData studentData = new StudentData();
studentData.add(new Student());
studentData.delete(new Student());
studentData.update(new Student());
Student s= studentData.query(1);
}
}
class Student {
}
class Teacher {
}
//自定义泛型接口
interface Data<T>{
void add(T t);
void delete(T t);
void update(T t);
T query(int id);
}
class StudentData implements Data<Student>{
@Override
public void add(Student student) {
}
@Override
public void delete(Student student) {
}
@Override
public void update(Student student) {
}
@Override
public Student query(int id) {
return new Student();
}
}
class TeacherData implements Data<Teacher>{
@Override
public void add(Teacher teacher) {
}
@Override
public void delete(Teacher teacher) {
}
@Override
public void update(Teacher teacher) {
}
@Override
public Teacher query(int id) {
return null;
}
}
Ⅳ.泛型方法、通配符、上下限
- 泛型方法
修饰符 <类型变量,类型变量,...> 返回值类型 方法名(形参列表){}
public static <T> void test(T t){}
- 泛型方法作用:泛型方法可以避免强制类型转换,在编译时就能够报错,同时能够确保方法接收参数的多样性,提升方法的复用性。
点击查看代码
public class GenericFunction {
public static void main(String[] args) {
//需求:打印任意数组的内容
Integer[] arr = {1,2,3,4,5};
print(arr);
Integer arr2=get(arr, 2);
System.out.println(arr2);
}
public static <T> void print(T[] arr){//泛型无返回值使用方法,打印任意数组类型T
for (T o : arr) {
System.out.print(o+" ");
}
}
public static <T> T get(T[] arr,int index){//泛型方法,返回值为T类型
return arr[index];//返回数组指定索引位置的元素
}
}
- 通配符(wildcard):
就是“?”,可以在“使用泛型”的时候代表一切类型;ETKV是在定义泛型的时候使用。(通配符是泛型类型的占位符)
- 泛型的上下限:
(1)泛型上限:? extends Car表示只能接受Car或者其子类。
(2)泛型下限:? super Car表示只能接受Car或者其父类。
点击查看代码
public class WildcardGeneric {
public static void main(String[] args) {
ArrayList<Car> cars = new ArrayList<>();
cars.add(new Car());
cars.add(new Bmw());
cars.add(new BYD());
run(cars);//输出:汽车在跑\n宝马在跑\n比亚迪在跑
ArrayList<Bmw> bmws = new ArrayList<>();
// run(bmws);//报错,因为集合里面只能是Car类
bmws.add(new Bmw());
SystemOut(bmws);//输出:BMW
}
public static void run(ArrayList<Car> cars){
//这里是虽然是父类,但是只能够访问Car类,不能访问子类,不像多态
for (Car car : cars) { car.run(); }
}
public static void SystemOut(ArrayList<? extends Car> cars){
//这里使用通配符上限,可以访问Car类,也可以访问Car子类
for (Car car : cars) {
System.out.println(car);
}
}
}
class Car{
public void run(){ System.out.println("汽车在跑"); }
@Override
public String toString() { return "Car"; }
}
class Bmw extends Car{...}//重写Car的run和toString
class BYD extends Car{...}//重写Car的run和toString
Ⅴ.泛型支持的类型-包装类
- 泛型不支持基本数据类型,如:int、char、boolean等,只能支持对象类型(引用数据类型)。
- 解释:泛型工作在编译阶段,编译阶段结束后不工作了,故泛型在编译后会被擦除,所有类型都会恢复成Object类型;而Object类型只能接收引用类型(Object只能指向对象)。
- 包装类:就是把基本类型的数据包装成对象的类型。
- 为了万物皆对象,并且泛型和集合都不支持基本类型,支持包装类。
- 包装类有8种,int->Integer,char->Character,其他的都是首字母大写。
| 基本数据类型 |
对应的包装类(引用数据类型) |
| byte |
Byte |
| short |
Short |
| int |
Integer |
| long |
Long |
| char |
Character |
| float |
Float |
| double |
Double |
| boolean |
Boolean |
- 手动包装,基本类型的数据包装成对象的方案:
(1)public Integer(int value):已过时
(2) public static Integer valueOf(int i)
- 自动包装
(1)自动装箱:基本数据类型可以自动转换为包装类型。
(2)自动拆箱:包装类型可以自动转换为基本数据类型。
- 包装类具备的其他功能
(1)可以把基本类型的数据转换成字符串类型。
public static String toString(double d)
public String toString()
(2)可以把字符串数值转换成数值本身对应的真实数据类型。
public static int parseInt(String s)
public static Integer valueOf(String s)
点击查看代码
public class GenericDemo6 {
public static void main(String[] args){
//把基本数据类型变成包装类对象。
//手工包装:
//Integer i=new Integer(100);//过时
Integer it1=Integer.valueOf(100);//推荐的
Integer it2=Integer.valueOf(100);//推荐的
System.out.println(it1==it2);//返回值true
//自动装箱成对象:基本数据类型的数据可以直接变成包装数据的对象,不需要额外做任何事情
Integer it11=130;
Integer it22=130;
System.out.println(it1==it22);//返回值false
//自动拆箱:把包装类型的对象直接给基本类型的数据
int i=it11;
System.out.println(i);
ArrayList<Integer> list=new ArrayList<>();
list.add(130);//自动装箱
list.add(120);//自动装箱
int rs=list.get(1);//自动拆箱
//包装类新增的功能:
//1.把基本类型的数据转换成字符串。
int j=23;
String rs1=Integer.toString(j);//”23”
System.out.println(rs1+1);//231
Integer i2=j;
String rs2=i2.toString();
System.out.println(rs2+1);//231
String rs3=j+"";//任意内容加“”得到字符串
System.out.println(rs3+1);//231
//2.把字符串数值转换成对应的基本数据类型(很有用)。
String str="98";
//int i1=Integer.parseInt(str);
int i1=Integer.valueOf( str);
System.out.println(i1+2);
String str2="98.8";
//double d=Double.parseDouble(str2);
double d=Double.valueOf(str2);
System.out.println(d+2);
}
}
(三)集合框架
Ⅰ.集合体系结构
- 集合是一种容器,用来装数据的,类似于数组,但集合的大小可变,开发中也非常常用。
- 两类集合:Collection(单列集合)、Map(双列集合)。
(1)Collection代表单列集合,每个元素(数据)只包含一个值。
(2)Map代表双列集合,每个元素包含两个值(键值对)。
Ⅱ.Collection集合
- Collection(祖宗接口)、List、Set和Map<K,V>只是接口,其子类是其实现类。
![屏幕截图 2025-09-09 182334]()
- Collection集合特点
(1)List系列集合:添加的元素是有序、可重复、有索引的。
-- ArrayList、LinekdList:有序、可重复、有索引。
(2)Set系列集合:添加的元素是无序、不可重复、无索引的。
-- HashSet:无序、不可重复、无索引;
-- LinkedHashSet:有序、不可重复、无索引;
-- TreeSet:按照大小默认升序排序、不可重复、无索引。
- Collection的常用功能:
(1)Cllection是单列集合的祖宗,它规定的方法(功能)是全部单列集合都会继承的。
(2)把集合中的元素,存储到数组中:
//把集合转换成数组
Object[] arr=list.toArray();
//把集合转换成字符串数组(拓展)
String[] arr = list.toArray(new String[0]);
String[] arr2=list.toArray(String[]::new);
| Collection方法名 |
说明 |
| public boolean add(E e) |
把元素e添加到集合中,并返回true |
| public void clear() |
清空集合中所有的元素 |
| public boolean remove(E e) |
删除集合中元素e,并返回true |
| public boolean contains(E e) |
判断集合中是否包含元素e,返回true |
| public boolean isEmpty() |
判断集合是否为空,返回true |
| public int size() |
返回集合中元素的个数 |
| public Object[] toArray() |
把集合中的元素,存储到数组中 |
- Collection集合的三种遍历方式
- Collection的遍历方式一:迭代器遍历
(1)迭代器是用来遍历集合的专用方式(数组没有迭代器),在Java中迭代器的代表是Iterator。
(2)Iterator e=name.iterator();
(3)通过迭代器获取集合的元素,如果取元素越界会出现NoSunchElementException异常。
Collection集合获取迭代器的方法
| 方法名称 |
说明 |
| Iterator< E > iterator() |
返回集合中的迭代器对象,该迭代器对象默认指向当前集合的第一个元素 |
Iterator迭代器中的常用方法
| Iterator |
说明 |
| boollean hasNext() |
询问当前位置是否有元素存在,存在返回true,不存在返回false |
| E next() |
获取当前位置的元素,并同时将迭代器对象指向下一个元素处。 |
点击查看代码
Collection<String> name = new ArrayList<>();
name.add("张三");name.add("李四");...//添加元素
Iterator<String> it= name.iterator();//得到集合的迭代器对象
while (it.hasNext()) {//.hasNext()判断集合中是否还有元素,有返回true,没有返回false
String next = it.next();
System.out.print(next+" ");//输出:张三 李四 ...
}
- Collection的遍历方式二:增强for循环
(1)增强for可以用来遍历集合或者数组。
(2)增强for遍历集合,本质就是迭代器遍历集合的简化写法。
(3)格式:for (元素的数据类型 变量名 : 数组或者集合){ }
Collection<String> c=new ArrayList<>();
...
for(String s : c){
System.out.println(s);
}
- Collection的遍历方式三:Lambda表达式
(1)得益于JDK 8开始的新技术Lambda表达式,提供了一种更简单、更直接的方式来遍历集合。
(2)Lambda表达式:集合对象名.forEach(E e)
需要使用Collection的如下方法来完成
| 方法名称 |
说明 |
| defoult void forEach(Consum<? super T> action) |
结合lambda遍历集合 |
点击查看代码
collection.forEach(new Consumer<String>() {//匿名内部类,函数接口,重写方法accept
@Override
public void accept(String s) {
System.out.print(s+" ");
}
});
name.forEach(s -> System.out.print(s+" "));////使用Lambda方法
name.forEach(System.out::println);//方法引用简化,每输出一个值都会换行
- 三种遍历方式区别
- 并发修改异常:遍历集合发同时又存在增删集合元素的行为时可能出现业务异常,这种现象被称之为并发修改异常。
- 解决并发修改异常问题的方案
(1)如果集合支持索引,可以使用for循环遍历,每删除数据后做i--;或者可以倒着遍历。
(2)可以使用迭代器遍历,并用迭代器提供的删除方法删除数据。
(3)注意:增强for循环/Lambda遍历均不能解决并发修改异常问题,因此它们只适合做数据的遍历,不适合同时做增删操作。
点击查看代码
Collection<String> list= new ArrayList<>();
list1.add("田五");list1.add("田六");...//添加元素
//迭代器
Iterator<String> it= list1.iterator();
while (it.hasNext()){
String name=it.next();
if(name.contains("田")){
it.remove();//这里是直接用迭代器自己方法删除指向的元素
//list1.remove(name);//要是这样删除就会报异常,会出现并发修改异常
}
}
//普通for
for(int i=0;i<list.size();i++){//或使用倒叙遍历
String name=list.get(i);
if(name.contains("田")){
list.remove(name);
//没有i--时,每次删除数据导致后一位数据前进,i有跨位的问题,导致漏删
i--;//加上,删除后后撤一步
}
}
//增强for
for(String s:list1){
if(s.contains("田")){
list1.remove(s);//出现并发修改异常
}
}
//Lambda表达式
list1.forEach(s->{
if (s.contains("田"))
list1.remove(s);//并发修改异常
);
一)List集合
-- List集合:包括ArrayList、LinkedKist;都是有序、可重复、有索引的;同时继承Collectio集合方法。
- List集合的特有方法
(1)List集合支持的遍历方式
①for循环(因为List集合有索引)
②迭代器
③增强for循环
④Lambda表达式
(2)List集合因为支持索引,所以多了很多与索引相关的方法,当然,Collection的功能List也都继承了。
| List方法名 |
说明 |
| void add(int index,E element) |
在指定位置插入指定的元素 |
| E remove(int index) |
删除指定索引处的元素,返回被删除的元素 |
| E set(int index,E element) |
修改指定位置的元素,返回被修改的元素 |
| E get(int index) |
获取指定索引处的元素 |
- ArrayList集合(基于数组,占内存少):
(1)ArrayList的底层原理:ArrayList底层是基于数组存储数据的。
(2)数组的特点
① 查询速度快(注意:是根据索引查询数据块):查询数据通过地址值和索引快速定位,查询任意数据耗时相同。
② 增删数据效率低:可能需要把后面很多的数据前移。
(3)注意:ArrayList集合new对象的时候位空数组,第一次添加数据时,会创建一个默认大小为10的数组,每当集合元素个数超过数组大小时,会扩容成原来的1.5倍。
- LinkedList集合(基于链表)(建队列/栈):
(1)LinkeList的底层原理:
① LinkeList底层是基于链表存储数据的。
② 实际上是基于双链表实现的。
(2)LinkedList特点:增删相对块,查询慢,首尾操作比较快。
(3)应用场景:可以用来设计队列(只在首位增删数据);可以用来设计栈(只在栈顶增删数据)。
(4)链表的特点
① 链表中的数据是一个一个独立的节点组成的,节点在内存中是不连续的,每个结点包含数据值和下一个结点地址。
② 查询数据慢,无论查询哪个数据都要从头开始找。
③ 链表增删相对快,同时双链表相对于单链表查询较快。
④ 双链表对首尾元素进行增删改查的速度是极快的。
(5)LinkedList新增了很多首尾操作的特有方法。
| 方法名称 |
说明 |
| public void addFirst(E e) |
在该列表开头插入指定的元素 |
| public void addLast(E e) |
将指定的元素追加到此列表的末尾 |
| public E getFirst() |
返回此列表中的第一个元素 |
| public E getLast() |
返回此列表中的最后一个元素 |
| public E removeFirst() |
从此列表中删除并返回第一个元素 |
| public E removeLast() |
从此列表中删除并返回最后一个元素 |
二)Set集合
- Set系列集合特点:无序,不重复,无索引。
(1)HashSet:无序,不重复,无索引
(2)LinkedHashSet:有序,不重复,无索引
(3)TreeSet:排序(默认大小升序排序),不重复,无索引
- 注意:Set要用到常用方法,基本上就是Collection提供的!!自己几乎没有额外新增一些常用功能!
- 应用场景:常用于去重,去重后,Set集合中的元素个数就是去重后元素个数。
- HashSet集合的底层原理:
没有重复的元素,无索引,耗内存(每个数组元素都挂着一串链表或一个红黑树),相对还是不错的集合。
(1)哈希值:
① 就是一个int类型的随机值,Java中每个对象都有自己的哈希值。
② Java在的所有对象,都可以调用Object类提供的hashCode方法,返回该对象自己的哈希值。
public int hashCode(): 返回对象的哈希码值
(2)对象哈希值特点:
① 同一个对象多次调用hashCode()方法返回的哈希值是相同的。
② 不同对象,它们的哈希值大概率不相等,但也可能会相等(哈希碰撞)。
(3)HashSet集合是基于哈希表存储数据的。
(4)哈希表:
① JDK8之前:哈希表=数组+链表
② JDK8开始:Hash表=数组+链表+红黑树。
③ 哈希表是一种增删改查数据,性能都很好的数据结构。
(5)JDK8之前的哈希表:数组+链表
① HashSet第一次添加数据,创建默认大小为16的数组,默认加载因子为0.75,数组名table。(如果要扩容,则到16*0.75=12时,扩容到原来两倍。)
② 使用元素的哈希值对数组的长度做运算计算出应存入的位置。
③ 判断当前位置是否为null,如果是null直接存入。
④ 如果不为null,表示有元素,则调用equals方法比较,相等,则不存;不相等,则存入数组。
-- JDK8之前,新元素存入数组,占老元素位置,老元素挂下面。
-- JDK8开始之后,新元素直接挂在老元素下面。
(6)JDK8开始的哈希表:数组+链表+红黑树(进一步提高了操作数据的性能)
-- JDK8开始,当链表长度大于8,且数组长度>=64时,自动将链表转成红黑树。
-- 红黑树是一种增删改查数据性能相对都较好的结构(自平衡二叉树)。
(7)HashSet集合元素去重复的机制:
-- 由于每一个对象的Hash值不一样,如果两个不同对象内的值相同,但是他们的Hash值不同(如果希望Set集合认为2个内容一样的对象是重复的,必须重写对象的hashCode()和equals()方法)。
点击查看代码
class Student {//重写hashCode()和equals()方法,可以自动生成
private String name;
private int age;
private String gender;
@Override
public boolean equals(Object o) {//内容的比较,只要内容一样,结果一定是true
if (this == o) return true;//this指当前对象,o指传入的对象,自己和自己比直接返回true
if (o == null || getClass() != o.getClass()) return false;//判断是否为空,是否为同一个对象相比较
Student student = (Student) o;//强转
return age == student.age && Objects.equals(name, student.name) && Objects.equals(gender, student.gender);//比较各个属性
}
@Override
public int hashCode() {
//保证不同的学生对象,如果内容一样返回的哈希值一定是一样的
return Objects.hash(name, age, gender);
}
//...Getter()、Setter()、toString()、有参无参构造方法省略
}
- LinkedHashSet集合的底层原理:
(1)依然基于哈希表(数组、列表、红黑树)实现的。
(2)但是,它的每个元素都额外的多了一个双链表的机制记录它的前后元素位置**。
(3)但是它每个结点更加占用内存。
- TreeSet集合:
(1)底层基于红黑树实现的排序:
① Integer,Double,默认按照数值本身的大小进行升序排序
② 对于字符串类型:默认按照首字符的编号升序排序。
③ 自定义类型(如Student对象)默认无法直接排序,需要重写Comparable接口的compareTo()方法,按照自定义的
点击查看代码
//TreeSet不能为自定义对象排序,因为不知道大小规则
//方案一:对象类实现一个Comparable比较接口,重写compareTo方法,指定大小比较规则
@Override//Student类需要继承Comparable<Student>
public int compareTo(Student o) {
//比较者:this
//被比较者:o
//规则1:若左边大于右边,返回正整数
//规则2:若左边小于右边,返回负整数
//规则3:若左边等于右边,返回0
//年龄升序
if(this.age>o.age){
return 1;
}else if(this.age<o.age){
return -1;
}
return 1;//如果为0的话,则会删除二者之一不显示,若为1的话返回this
//return this.age-o.age;//升序
//return o.age-this.age;//降序
}
//方案二:public TreeSet(Comparator c)集合自带比较器Comparator对象,指定比较规则
TreeSet<Student> s2=new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// return o1.getAge()-o2.getAge();//若为double则强转有风险
// return Double.compare(o1.getAge(),o2.getAge());//使用封装函数
if(o1.getAge()>o2.getAge()) return 1;
else if(o1.getAge()<o2.getAge()) return -1;
else return 1;//这样写更加保险并且操作性更高,可以免掉去重
}
});
TreeSet<Student> s3=new TreeSet<>((o1, o2) -> Double.compare(o1.getAge(),o2.getAge()));//Lambda表达式简化
TreeSet<Student> s4=new TreeSet<>(Comparator.comparingInt(Student::getAge));//方法引用简化
三)小结
- 若想记住元素添加顺序,需存储重复元素,又要频繁根据索引查询数据:用ArrayList集合(最常用)。
- 若想记住元素添加顺序,且增删首尾情况较多:用LinkedList集合。
- 不在意元素顺序,也无重复元素需要存储,只希望增删改查都快:用HashSet集合(常用·)。
- 希望记住元素添加顺序,也没有重复元素需要存储,且希望增删改查都快:用LinkedHashSet集合。
- 若想对以元素进行排序,也无重复元素需要存储,且希望增删改查都快:用TreeSet集合。
Ⅲ.Map集合
一)体系特点-常用方法
- 认识Map集合
(1)Map集合也叫做“键值对集合”,格式:{key1=value1,key2=value2,key3=value3,...}
Map<String,Integer>map=new HashMap<>();//一行经典代码
(2)Map集合的所有键不允许重复,但值可以重复,键与值是一一对应的,每一个键只能找到自己对应的值。
(3)应用场景:需要存储一一对应的数据时,就可以考虑使用Map集合来做。
- Map集合体系:
![屏幕截图 2025-09-12 202129]()
(1)Map集合特点:
注意:都是由键决定,键值对都可以是null(TreeMap除外),值只是一个附属品,值是不做要求的(可以重复)。
① HashMap(由键决定特点):无序、不重复、无索引;(用的最多)
② LinkedHashMap(由键决定特点):有序、不重复、无索引;
③ TreeMap(由键决定特点):按照大小默认升序排序、不重复、无索引。
- Map集合的常用方法
-- Map集合是双列表集合的祖宗,它的功能是全部双列表集合都可以继承过来使用的。
| 方法名称 |
说明 |
| public V put(K key,V value) |
添加元素 |
| public int size() |
获取集合的大小 |
| public void clear() |
清空集合 |
| public boolean isEmpty() |
判断集合是否为空,为空返回true,反正 |
| public V get(Object key) |
根据键获取对应值 |
| public V remove(Object key) |
根据键删除整个元素 |
| public boolean containsKey(Object key) |
判断是否包含某个键 |
| public boolean containsValue(Object value) |
判断是否包含某个值 |
| public Set keySet() |
获取全部键的集合(Set集合不可重复) |
| public Collection values() |
获取Map集合的全部值(Collection集合可重复) |
| public boolean equals(Object o) |
判断是否相等 |
| Set<Map.Entry<K,V>> entrySet() |
获取全部"键值对"的集合(Set集合不可重复) |
| default void forEach(BiConsumer<? super K,? super V> action) |
结合Lambda表达式遍历Map集合 |
二)Map集合遍历方式
- 键找值:先获取Map集合全部的键,再通过遍历建来找值。(Set keySet()、V get(Object key))
//遍历
Set<String> keySet=map.keySet();
for(String key:keySet){
//根据键去找值
Integer value=map.get(key);
System.out.println(key+"="+value);
}
- 键值对:把键值对整体包装成一个Entry的实现类对象进行遍历(Set<Map.Entry<K,V>> entrySet())
点击查看代码
//键值对遍历
//把Map集合转换成Set集合,里面的元素类型都是键值对类型(Map.Entry<String,Integer>)
Set<Map.Entry<String,Integer>> entrySet=map.entrySet();
for(Map.Entry<String,Integer> entry:entrySet){
String key=entry.getKey();
Integer value=entry.getValue();
System.out.println(key+":"+value);
// System.out.println(entry);//一步到位,和上面三行代码一样功能
}
//一步到位,不需要在for外创建Set对象entrySet()
for(Map.Entry<String,Integer> entry:map.entrySet()){
System.out.println(entry);
}
- Lambda表达式
default void forEach(BiConsumer<? super K,? super V> action)方法,结合Lambda表达式遍历集合
点击查看代码
//Lambda 遍历
map.forEach(new BiConsumer<String, Integer>() {
@Override
public void accept(String key, String value) {
System.out.println(key+"="+value);
}
});
//函数式接口的匿名内部类
map.forEach((key,value)-> System.out.println(key+"="+value));
三)Map集合实现类
- HashMap集合(用的最多)的底层原理
(1)实际上:原来学的Set系列集合的底层就是基于Map实现的,只是Set集合在的元素只要健数据,不要值数据而已。
(2)HashMap跟HashSet的底层原理是一模一样的,都是基于哈希表实现的。
- LinkedHashMap
(1)LinkedHashSet集合的底层原理就是LinkedHashMap。
(2)底层数据结构依然是基于哈希表实现的,每个键值对元素又额外的多了一个双链表的机制记录元素顺序(保证有序)。
- TreeMap
(1)特点:不重复、无索引、可排序(按照键的大小默认升序排序,**只能对键排序。
(2)原理:TreeMap跟TreeSet集合的底层原理是一样的,都是基于红黑树实现的排序。
(3)TreeMap集合同样也支持两种方式来指定排序规则
① 让类实现Comparable接口,重写比较规则。
② TreeMap集合有一个有参构造器,支持创建Comparator比较器对象,以便用来指定比较规则。
四)Map综合小案例
MapTest代码
public class MapTest {
public static void main(String[] args) {
List<String> location = new ArrayList<>();
String[] locat = {"北京", "上海", "广州", "深圳"};
Random random = new Random();
for (int i = 1; i <= 80; i++) {
int index = random.nextInt(locat.length);
location.add(locat[index]);
}
// System.out.println(location);
//最终统计为键值对集合
Map<String, Integer> map = new HashMap<>();
for (String str : location) {
if (map.containsKey(str)) {//判断是否包含键
map.put(str, map.get(str) + 1);
} else {
map.put(str, 1);//键不存在,添加键值对
}
}
map.forEach((k, v) -> System.out.println(k + ":" + v));
(四)Stream流(辅助集合)
Ⅰ.体验-获取Stream流
- 认识Stream流
(1)是Jdk8开始新增的一套API(java.util.stream.*),可以用于操作集合或者数组的数据。
(2)优势:Stream流大量的结合了Lambda的语法风格来编程,功能强大,性能高效,代码简洁,可读性好。
- Stream流的使用步骤:
(1)创建数据源(集合/数组),获取数据源的Stream流
(2)获取一个流中的数据,调用Stream流的方法对数据进行操作,得到一个结果(对数据处理计算)。
(3)获取处理的结果(遍历统计收集到一个新的集合中返回)。
点击查看代码
List<String> list = List.of("张三峰", "张三", "流水","qiqi");
//传统方案找出姓张的三个字的人
List<String> newlist=new ArrayList<>();
//s.startsWith("张")判断字符串是否以张开头。
for (String s : list) {
if(s.startsWith("张")&&s.length()==3){
newlist.add(s);
}
}
System.out.println(newlist);
//stream流
list.stream().filter(s->s.startsWith("张"))
.filter(s->s.length()==3)
.forEach(System.out::println);
//stream流收集传回对象
List<String> newlist2=list.stream().filter(s->s.startsWith("张")).filter(s->s.length()==3).collect(Collectors.toList());
System.out.println(newlist2);
- 获取Stream流
- Stream流本身是一个接口,但Stream流直接调用方法即可,不需要new一个实现类。
- 获取集合的Steam流:
| Collection提供的如下方法 |
说明 |
| default Stream stream() |
获取当前集合对象的Stream流 |
| Arrays类提供的如下方法 |
说明 |
| public static Stream stream(T[] array) |
获取当前数组的Stream流 |
| Stream类提供的如下方法 |
说明 |
| public static stream of(T.. values) |
获取当前接收数据的Stream流 |
点击查看代码
public static void getstreams(){
//获取Stream流
//1、集合stream流,所有单列结合获取Stream流都要调用stream方法
Collection<String> list = new ArrayList<>();
Stream<String> liststream = list.stream();
//2、Map集合获取Stream流,获取键流,值流,键值对流
Map<String, String> map=new HashMap<>();
Stream<String> valueStream = map.values().stream();//值流
Stream<String> keyStream = map.keySet().stream();//键流
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();//键值对流
//3、获取数组的流
String[] strs = {"a","b","c"};
Stream<String> stream = Arrays.stream(strs);
Stream<String> stream1 = Stream.of(strs);
Stream<String> stream2 = Stream.of("a","b","c");//等价于Arrays.stream(strs)
}
Ⅱ.Stream流常用中间方法
中间方法指的是调用完成后会返回新的Stream流,可以继续使用(支持链式编程)。
| Stream流提供的中间方法 |
说明 |
| Stream filter(Predicate<? super T> predicate)` |
筛选,过滤,过滤流中的元素,返回一个新流,该流包含所有满足指定条件的元素 |
| Stream sorted() |
对元素进行升序排序 |
| Stream sorted(Comparator<? super T> comparator) |
按照指定规则排序 |
| Stream limit(long maxSize) |
获取前n个元素,返回一个新流 |
| Stream skip(long n) |
跳过前n个元素,返回一个新流 |
| Stream distinct() |
去除流中重复元素(基本类型已经重写HashCode和equals方法,所以可以去除自定义对象重复元素) |
| Stream map(Function<? super T,? extends R> mapper) |
对元素进行加工,并返回对应的新流(映射,把流中的元素按照一定的规则映射到另一个流中) |
| static Stream concat(Stream<? extends T> a, Stream<? extends T> b) |
合并a和b两个流为一个流 |
| Stream peek(Consumer<? super T> action)` |
对每个元素进行操作 |
点击查看代码
public static void getdouble(){
List<Double> list = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5,5.5, 6.6, 7.7, 8.8, 9.9, 10.1);
//默认升序,去重
list.stream().sorted().distinct().forEach(System.out::println);
System.out.println("=====");
//只需要最大的两个元素
list.stream().sorted(Comparator.reverseOrder()).distinct().limit(2).forEach(System.out::println);
System.out.println("=====");
//跳过前两个
list.stream().skip(2).forEach(System.out::println);
System.out.println("=====");
//加工方法
list.stream().map(s->"加十分后"+(s+10)).forEach(System.out::println);
System.out.println("=====");
//合并流
Stream<Double> stream1 = Stream.of(1.1, 2.2, 3.3);
//统计合并的流的长度
System.out.println(Stream.concat(stream1,list.stream()).count());
}
Ⅲ.Stream流常用终结方法
终结方法是指调用完成后,不会返回新的Stream流,没法继续使用流。
| Stream流终结方法 |
说明 |
| void forEach(Consumer<? super T> action) |
遍历,对此流运算后的元素执行遍历 |
| long count() |
统计流运算后的元素个数 |
| Optional min(Comparator<? super T> comparator) |
返回流运算后的最小元素(给一个比较器),如果流为空,返回Optional.empty() |
| Optional max(Comparator<? super T> comparator) |
返回流运算后的最大元素,如果流为空,返回Optional.empty() |
//Optional的Stream流终结方法
Optional<Double> max=list.stream().max((list1,list2)->Double.compare(list1,list2));
System.out.println(max);//输出最大的值
- 收集Stream流
(1)收集Stream流:就是把Stream流操作后的结果转回到集合或者数组中去返回。
(2)Stream流:方便操作集合/数组的手段,数组/集合:才是开发中的目的。
(3)一个流只能收集一次,所以想要把流转到两个集合/数组中去只能重新创建流,或者使用函数赋值。
| Stream流收集方法 |
说明 |
| R collect(Collector<? super T, A, R> collector) |
把流处理后的结果收集到一个指定的集合中去 |
| Object[] toArray() |
把流处理后的结果收集到一个数组中去,返回一个Object数组,元素类型为流中元素的类型 |
| Collactors工具类提供了具体的收集方式 |
说明 |
| public static collector toList() |
把流中的元素收集到List集合中,返回一个List集合,元素类型为流中元素的类型 |
| public static collector toSet() |
把流中的元素收集到Set集合中,返回一个Set集合,元素类型为流中元素的类型 |
| public static collector toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)` |
把流中的元素收集到Map集合中,返回一个Map集合,元素类型为流中元素的类型,keyMapper和valueMapper是函数式接口,用来获取键值对 |
Ⅳ.综合案例
- 方法中的可变参数:
(1)就是一种特殊形参,定义在方法、构造器的形参列表里,格式是:数据类型...参数名称;
(2)特点:可以不传数据给它;可以传一个或者同时传多个数据给它;也可以传一个数组给它。
(3)好处:常常用来灵活的接收数据。
(4)可变参数在方法内部就是一个数组,可变参数在形参列表中只能有一个,只能放在在形参列表中的最后面。
- Collectors工具类:
(1)操作集合的工具类。
(2)Collection提供的常用静态方法
给集合批量添加数据:Collections.addAll(集合对象,元素1,元素2,元素3...);
打乱List集合中的元素顺序:Collections.shuffle(List对象);
对List集合中的元素进行升序排序:Collections.sort(List对象);(注意:本方法可以直接对自定义类型的List集合排序,但自定义类型必须实现了Comparable接口,指定了比较规则才可以)
对List集合中的元素,按照比较器对象指定的规则进行排序:Collections.sort(List对象,比较器);
(五)文件操作与递归
Ⅰ.File-IO流-课程介绍
- 存储数据的方案:
(1)变量、数组、对象、集合:这些数据容器都在内存中,一旦程序结束,或者断电,数据就没有了!
(2)文件:
① 文件可以长久保存数据。
② 文件在电脑磁盘中保存,即便断电,或者程序终止,文件中的数据数据也不会丢失。
- File
(1)File是java.io.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件或文件夹)。
(2)注意:FIle只能对文件本身进行操作,不能读写文件里面存储的数据。
- IO流
用于读写数据的(可以读写文件,或网络中的数据...)。
Ⅱ.File
- File类的对象可以代表文件/文件夹,并可以调用其提供的方法对象文件进行操作。
- 创建File对象
| 构造器 |
说明 |
| public File(String pathname) |
根据文件路径创建文件对象 |
| public File(String parent,String child) |
根据父路径和子路径名字创建文件对象 |
| public File(File parent,String child) |
根据父路径对应的文件对象和子路径名字创建文件对象 |
- 注意:
(1)File对象及可以代表文件、也可以代表文件夹。
(2)File封装的对象仅仅是一个路径名,这个路径名可以是存在的,也允许是不存在的。
- File提供的判断文件类型、获取文件信息功能
| 方法名称 |
说明 |
| public boolean exists() |
判断当前文件对象,对应的文件路径是否存在,存在返回true |
| public boolean isFile() |
判断当前文件对象指代的是否是文件,是文件返回true,反之。 |
| public boolean isDirectory() |
判断当前文件对象指代的是否是文件夹,是文件夹返回true |
| public String getName() |
获取文件的名称(包含后缀) |
| public long length() |
获取文件的大小,返回字节个数 |
| public long lastModified() |
获取文件的最后修改时间 |
| public String getPath() |
获取创建文件对象时,使用的路径 |
| public String getAbsolutePath() |
获取绝对路径 |
- 绝对路径、相对路径
(1)绝对路径:从盘符开始File file1=new File("D:\\itheima\\a.txt");
(2)相对路径:不带盘符,默认直接到当前工程下的目录寻找文件。File file3=new File("模块名\\a.txt");
- File提供的创建和删除文件的方法
File类创建文件的功能
| 方法名称 |
说明 |
| public boolean createNewFile() |
创建一个新的空文件 |
| public boolean mkdir() |
只能创建一级文件夹 |
| public boolean mkdirs() |
可以创建多级文件夹 |
File类删除文件的功能
| 方法名称 |
说明 |
| public boolean delete() |
删除文件、空文件夹 |
注意:delete方法默认只能删除文件和空文件夹,删除后的文件不会进入回收站。
7. File提供的遍历文件夹的方法
(1)File类提供的遍历文件夹的功能
| 方法名称 |
说明 |
| public String[] list() |
获取当前目录下所有的”一级文件名称“到一个字符串数组中去返回。 |
| public File[] listFiles() |
获取当前目录下所有的”一级文件对象“到一个文件对象数组中去返回(重点) |
(2)使用listFiles()方法时的注意事项:
① 当主调是文件,或者路径不存在,返回null。
② 当主调是空文件夹,返回长度为0的数组。
③ 当主调是一个有内容的文件夹时,将里面的所有一级文件和文件夹的路径放在File数组中返回。
④ 当主调是一个文件夹,且里面有隐藏文件时,将里面的所有文件和文件夹的路径放在File数组中返回,包含隐藏文件。
⑤ 当主调是一个文件夹,但是没有权限访问该问价夹时,返回null。
点击查看代码
public static void startFile(){
//创建File对象,获取某个文件的信息,默认在工程下找
String path = "resource\\mapper.png";//或是String path = "resource/mapper.png";
String path2 = "resource/exists.txt";
java.io.File file = new java.io.File(path);
System.out.println("文件名:"+file.getName());
System.out.println("文件是否存在:"+file.exists());
System.out.println("文件是否可读:"+file.canRead());
System.out.println("文件是否可写:"+file.canWrite());
//获取字节个数
System.out.println("文件大小:"+file.length());
System.out.println("文件大小(KB):"+new DecimalFormat("0.00").format(file.length()/1024.0) +"KB");
System.out.println("文件大小(MB):"+new DecimalFormat("0.00").format(file.length()/(1024.0*1024))+"KB");
//获取文件名字
System.out.println("文件名字:"+file.getName());
//是不是文件,或文件夹
System.out.println("是不是文件:"+file.isFile());
System.out.println("是不是文件夹:"+file.isDirectory());
//获取绝对路径
System.out.println("绝对路径:"+file.getAbsolutePath());
//获取使用路径
System.out.println("使用相对路径:"+file.getPath());
//获取最后修改时间,并格式化时间
System.out.println("最后修改时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(file.lastModified()));
/*
//创建对象代表不存在的文件路径
java.io.File file2 = new java.io.File(path2);
if(!file2.exists()) {
try {
file2.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
}else {
System.out.println("文件已存在");
}
//创建对象代表不存在文件夹路径
java.io.File file3 = new java.io.File("resource/test");//只能创建一级文件夹
if(!file3.exists()) {
file3.mkdir();
}else {
System.out.println("文件夹已存在");
}
//创建多级文件夹
java.io.File file4 = new java.io.File("resource/test/test2/test3");//创建多级文件夹
if(!file4.exists()) {
file4.mkdirs();
}else {
System.out.println("文件夹已存在");
}
//删除操作
System.out.println("删除"+path2+"结果:"+file2.delete());
System.out.println("删除resource/test/test2/test3结果:"+file4.delete());
//只能删除空文件夹,不能删除非空文件夹,,删除后的文件不会进回收站
//要想删除有文件的文件夹,只能用递归算法写方法删除
*/
//获取某个文件夹下的所有一级文件名称
java.io.File file5 = new java.io.File("resource");
String[] fileNames = file5.list();
System.out.println("文件夹下文件个数:"+fileNames.length);
System.out.println("文件夹下文件名称:");
for(String name:fileNames){
System.out.println(name);
}
//获取一级文件对象,还可以对文件对象进行操作
java.io.File[] files = file5.listFiles();
System.out.println("文件夹下文件个数:"+files.length);
System.out.println("文件夹下文件名称与路径:");
for(java.io.File f:files){//获取绝对路径
System.out.println(f.getName()+":"+f.getAbsolutePath());
}
/**
* 对listFiles()方法注意
* 当主调是文件,或者路径不存在,返回null
* 主调是空文件夹,返回长度为0的数组
* 主调是一个有内容的文件夹时,将里面的所有一级文件和文件夹的路径放在File数组中返回
* 当主调是一个文件夹,且里面有隐藏文件时,将里面的所有文件和文件夹的路径放在File数组中返回,包含隐藏文件
* 当主调是一个文件夹,但是没有权限访问该问价夹时,返回null。
*/
}
Ⅲ.方法递归
一)认识递归的形式
- 什么是递归
(1)递归是一种算法,在程序设计语言中广泛应用。
(2)从形式上说:方法调用自身的形式称为方法递归(recursion)。
(4)递归的形式:
① 直接递归:方法自己调用自己。
② 间接递归:方法调用其他方法,其他方法又回调方法自己。
(5)使用方法递归时如果没有控制好终止,会出现递归死循环,导致栈内存溢出错误。
二)递归算法和其执行流程
- 递归算法三要素(以阶乘为例):
(1)递归的公式:f(n)=f(n-1)*n
(2)递归的终结点:f(1)
(3)递归的方向要走向终结点。(有明确公式规定的)
- 无明确公式:如在D盘搜索WeChat.exe文件
(1)获取D盘下的所有一级文件对象
(2)遍历全部一级文件对象,判断是否是文件WeChat.exe
(3)若为文件,判断是否为自己想要的
(4)若为文件夹,继续调用1步骤
点击查看代码
import java.io.File;
import java.io.IOException;
public class FileSearchTest4 {
public static void main(String[] args){
try{
File dir=new File("D:/");
searchFile(dir,"WeChat.exe");
}catch(Exception e){
e.printStackTrace();
}
}
public static void searchFile(File dir, String fileName) throws Exception{
//判断极端情况
if(dir==null||!dir.exists()||dir.isFile()){
return;
}
//获取目录下的所有一级文件夹对象
File[] files=dir.listFiles();
//判断当前目录下是否存在一级文件对象,存在才可以遍历
if(files!=null&&files.length>0){
for(File file:files){
if (file.isFile()) {
if(file.getName().equals(fileName)){//.comtains:是否包含 .equals:是否相等
System.out.println("找到目标文件路径:"+file.getAbsolutePath());
Runtime r=Runtime.getRuntime();//获取当前运行环境
r.exec(file.getAbsolutePath());//运行文件
}
}else {
searchFile(file, fileName);
}
}
}
}
}
Ⅳ.字符集(可以加密)
一)常见字符集介绍
- 常见字符集
(1)标准ASCII字符集
① ASCII字符集:美国信息交换标准代码,包括了英文、符号等。
② 标准ASCII使用一个字节存储一个字符,首位为0,因此,共可表达128个字符。
(2)GBK字符集(汉字内码扩展规范,国标)
① 汉字编码字符集,包含了21844个汉字等字符,GBK中一个中文字符编码成两个字节的形式存储。
② 注意:GBK兼容了ASCII字符集。
③ GBK规定:汉字的第一个字节的第一位必须是1。
(3)Unicode字符集(统一码,也叫万国码):Unicode是国际组织制定的,可以容纳世界上所有文字、符号的字符集。
(4)UTF-32:4个字节表示一个字符,共42亿个字符,兼容GBK字符集。
(5)UTF-8字符集
① 是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4字节。
② 英文字符、数字等只占1字节(兼容标准ASCII编码),汉字字符占用3个字节。
③ UTF-8编码方式(二进制):0开头为1字节区(ASCII码),110xxxxx 10xxxxxx为2字节区,1110xxxx 10xxxxxx 10xxxxxx为3字节区,11110xxx 10xxxxxx 10xxxxxx 10xxxxxx为4字节区。
④ 注意:技术人员在开发时都应该使用UTF-8编码!
- 注意:
(1)字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码。
(2)英文,数字一般不会乱码,因为很多字符集都兼容了ASCII编码。
二)编码-解码操作
- 使用程序对字符集进行编码和解码操作
对字符的编码
| String提供了如下方法 |
说明 |
| byte[ ] getByte( ) |
使用平台默认的字符集将该String编码为一系列字节,将结果存储到新的字节数组中 |
| byte[ ] getBytes(String charsetName) |
使用指定的字符集将该String编码为一系列字节,将结果存储到新的字节数组中 |
对字符的解码
| String提供了如下方法 |
说明 |
| String(byte[ ] bytes) |
通过使用平台默认的字符集解码指定的字节数组来构造新的String |
| String(byte[ ] bytes,String charsetName) |
通过指定的字符集解码指定的字节数组来构造新的String |
点击查看代码
String name="你好,Rasion";//可以用来加密
byte[] bytes=name.getBytes("GBK");//通过GBK编码,如果没有写为默认编码,默认为UTF-8
System.out.println(Arrays.toString(bytes));//打印字节数组
System.out.println(new String(bytes,"GBK"));//通过GBK解码
(六)IO流
Ⅰ.认识IO流
- I指Input,称为输入流:负责把数据读到内存中去。
- O指Output,称为输出流:负责写数据出去。
- 应用场景:写入读出数据。
- IO流的分类:
(1)按照流的方向分为——输入流和输出流。
(2)按照流的内容分为——字节流(适合操作所有类型文件)和字符流(只适合操作纯文本文件)。
- IO流的体系:(以下四类为抽象类)
(1)字节输入流:InputStream(读字节数据),实现类:FileInputStream
(2)字节输出流:OutputStream(写字节数据出去),实现类:FileOutputStream
(3)字符输入流:Reader(读字符数据),实现类:FileReader
(4)字符输出流:Writer(写字符数据出去),实现类:FileWriter
Ⅱ.字节流
- 作用:以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中去。
| 构造器 |
说明 |
| public FileInputStream(File file) |
创建字节输入流管道与源文件接通 |
| public FileInputStream(String pathname) |
创建字节输入流管道与源文件接通 |
| 方法名称 |
说明 |
| public int read() |
每次读取一个字节返回,如果发现没有数据可读会返回-1 |
| public int read(byte[] buffer) |
每次用一个字节数组去读取数据,返回字节数组读取了多少个字节,如果发现没有数据可读会返回-1 |
- 注意事项:
(1)使用FileInputStream每次读取一个字节,读取性能较差,并且读取汉字输出会乱码。
(2)使用FileInputStream每次读取多个字节,读取性能得到了提升,但读取汉字输出还是会乱码。
- 使用字节流读取汉字,如何保证输出不乱码?
- 定义一个与文件一样大的字节数组,一次性读取完文件的全部字节。
- Java官方为InputStream提供了如下方法,可以直接把文件的全部字节读取到一个字节数组中返回。
| 方法名称 |
说明 |
| public byte[] readAllBytes() thows IOException |
直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回 |
- 如果文件过大,创建的字节数组也会过大,可能引起内存溢出。
点击查看代码
public static void readFile() throws Exception {//文件字节输入流
//文件字节输入流读取文件中的字节数组到内存中去
//1、创建文件字节输入流对象
//InputStream is = new FileInputStream(new File("resource/mapper.png"));
InputStream is=FileInputStream("resource/mapper.png");//简化写法
/* //2、读取文件中字节并输出
int a;
while ((a = is.read()) != -1) {
System.out.print((char) a);
}//每次读取一个字节:性能较差,读取汉字一定会乱码
*/
/*
//每次读多个字节
byte[] b = new byte[3];
//定义一个变量存储每次读取的字节个数
int len;
while ((len = is.read(b)) != -1) {//每次读取多个字节,与文件交互少,单读汉字有可能被截断
System.out.print(new String(b, 0, len));//把读到的字节数组转换成字符串输出,读取多少倒多少出来
}
*/
//定义一个与文件一样大的字节数组,这样用字节读取汉字就不会乱码,只适合读小文件
byte[] bytes=is.readAllBytes();
String rs=new String(bytes);
System.out.println(rs);//用readAllBytes()方法一次性读取全部字节,避免乱码
is.close();//3、释放资源
}
- 读取文字适合用字符流;字节流适合做数据的转移,如:文件的复制。
二)FileOutputStream(文件字节输出流)
- 作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去。
| 构造器 |
说明 |
| public FileOutputStream(File file) |
创建字节输出流管道与源文件对象接通 |
| public FileOutputStream(String filepath) |
创建字节输出流管道与源文件对象接通 |
| public FileOutputStream(File file,boolean append) |
创建字节输出流管道与源文件对象接通,可追加数据 |
| public FileOutputStream(String filepath,boolean append) |
创建字节输出流管道与源文件对象接通,可追加数据 |
| 方法名称 |
说明 |
| public void write(int a) |
写一个字节出去 |
| public void write(byte[] buffer) |
写一个字节数组出去 |
| public void write(byte[] buffer,int pos,int len) |
写一个字节数组的一部分出去 |
| public void close() throws IOException |
关闭流 |
- 字节输出流如何实现写出去的数据可以换行?
os.write("\r\n".getBytes());
点击查看代码
public static void writeFile() throws Exception{//字节输出流,不覆盖要加true
/* //文件字节输出流将字节数组写入文件中,当文件字节输出流输出后,文件原本的内容会被覆盖
//1、创建文件字节输出流对象,覆盖掉原文件
OutputStream os = new FileOutputStream("resource/hello.txt");
//2、将字节数组写入文件中
os.write(new byte[]{'a',100,'A',54,64});
byte[] bytes = "hello world".getBytes();
os.write(bytes,0,bytes.length);
//3、关闭文件输出流对象
os.close();
*/
//追加数据
FileOutputStream fileo = new FileOutputStream("resource/hello.txt",true);
fileo.write("hello world".getBytes());
fileo.close();//关闭文件输出流对象管道,释放资源
}
三)文件复制
- 字节流非常适合做文件的复制操作
- 任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题!
点击查看代码
public class CopyDemo1 {
public static void main(String[] args){
try{
CopyFile("D:\\20984\\123.txt","D:/123.txt");
}catch(Exception e){
e.printStackTrace();
}
}
public static void CopyFile(String srcPath,String destPath) throws Exception{
//1.创建一个文件字节输入流管道与源文件接通
InputStream fis=new FileInputStream(srcPath);
OutputStream fos=new FileOutputStream(destPath);
//2.读取一个字节数组,写入一个字节数组
byte[] buffer=new byte[1024];
int len;
while((len=fis.read(buffer))!=-1){
fos.write(buffer,0,len);//读取多少给字节,就写入多少个字节
}
System.out.println("复制完毕!");
//3.关闭流
fos.close();
fis.close();
}
}
四)资源释放问题
- 资源释放的方案:try-catch-finally
try{
...
...
}catch(IOException e){
e.printStackTrace();
}finally{
...释放资源并检测异常
}
- finally代码区的特点:无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finall区,除非JVM终止。
- 作用:一般用于在程序执行完成后进行资源的释放操作(专业级做法)。
- JDK7开始提供了更简单的资源释放方案:
try(定义资源1;定义资源2;...){
可能出现异常的代码;
}catch(异常类名 变量名){
异常的处理代码;
}
- 该资源使用完毕后,会自动调用其close()方法,完成对资源的释放!
- ( )中只能放置资源,否则报错
- 资源一般指的是最终实现了AutoCloseable接口
点击查看代码
//复制文件:文件->创建字节输入流管道->read->字节数组->write->创建字节输出流管道->另一个文件
public static void copyFile(){//复制文件的抛出异常
FileOutputStream fos = null;
FileInputStream fis = null;//定义流为空,确保finally内部能够使用
try {
fis = new FileInputStream("resource/mapper.png");//给流一个对象
fos = new FileOutputStream("resource/hello.png");
byte[] b = new byte[1024];
int len;
while ((len = fis.read(b)) != -1) {
fos.write(b, 0, len);
}
System.out.println("复制完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后一定会执行一次:即便程序出现异常!
try{if(fos!=null)fos.close();}//释放资源也要检测异常,确保资源完美关闭完
catch (Exception e){e.printStackTrace();}
try{if(fos!=null)fis.close();}
catch (Exception e){e.printStackTrace();}
}
}
//抛出异常更加优雅
public static void copyFile() throws Exception {
try(//这里只能放资源对象,用完后会自动关闭管道,不用finnally
FileOutputStream fis = new FileInputStream("resource/mapper.png");//管道流定义
FileInputStream fos = new FileOutputStream("resource/hello.png");)
{ byte[] b = new byte[1024];
int len;
while ((len = fis.read(b)) != -1) {
fos.write(b, 0, len);
}
System.out.println("复制完成");
} catch (Exception e) {
e.printStackTrace();
}
}
Ⅲ.字符流
字符流适合做数据的处理,如文本的读取。
一)FileReader(文件字符输入流)
- 作用:以内存为基准,可以把文件中的数据以字符的形式读入到内存中去。
| 构造器 |
说明 |
| public FileReader(File file) |
创建字符输入流管道与源文件接通 |
| public FileReader(String pathname) |
创建字符输入流管道与源文件接通 |
| 方法名称 |
说明 |
| public int read() |
每次读取一个字符返回,如果发现没有数据可读会返回-1 |
| public int read(char[] buffer) |
每次用一个字符数组去读取数据,返回字符数组读取了多少个字符,如果发现没有数据可读会返回-1 |
二)FileWriter(文件字符输出流)
- 作用:以内存为基准,把内存中的数据以字符的形式写出到文件中去。
| 构造器 |
说明 |
| public FileWriter(File file) |
创建字符输出流管道与源文件对象接通 |
| public FileWriter(String filepath) |
创建字符输出流管道与源文件对象接通 |
| public FileWriter(File file,boolean append) |
创建字符输出流管道与源文件对象接通,可追加数据 |
| public FileWriter(String filepath,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) |
写入字符数组的一部分 |
- 字符输出流的注意事项:字符输出流写出数据后,必须刷新流,或者关闭流,写出的数据才能生效。(先写在内存缓冲区,刷新.flush()缓冲区,才能写出数据,降低磁盘压力)
| 方法名称 |
说明 |
| public void flush() throws IOException |
刷新流,就是将内存中缓存的数据立即写到文件中去生效! |
| public void close() throws IOException |
关闭流的操作,包含了刷新! |
点击查看代码
public static void readFile() throws Exception {
//文件字符输入流读取文件中的字节数组到内存中去
try(Reader fr = new FileReader("resource/hello.txt");){
char[] chars = new char[3];//创建一个数组,用来存储读取的字符数组
int len = fr.read(chars);
while (len!=-1){
System.out.print(new String(chars,0,len));
len = fr.read(chars);
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void writeFile(){//字符输出流,不覆盖要加true
try(Writer writer = new FileWriter("resource/hello.txt", true)){
//异常抛出包含关闭流writer.close()的操作,可以不用写。
writer.write("\n");
writer.write("FileWirter",1,3);//写一部分出去
writer.flush();//刷新缓冲区,写出数据
}catch (Exception e){e.printStackTrace();}
}
Ⅳ. 缓冲流
- 字节缓冲输入流:BufferedInputStream
- 字节缓冲输出流:BufferedOutputStream
- 字符缓冲输入流:BufferedReader
- 字符缓冲输出流:BufferedWriter
一)缓冲字节流
- 两种字节缓冲流如何使用(包装)?
InputStream is=new FileInputStream(source);
BufferedInputStream bis=new BufferedInputStream(is);
OutputStream os=new FileOutputStream(destination);
BufferedOutputStream bos=new BufferedOutputStream(os);
- 作用:可以提高字节输入输出流读写数据的性能。
- 原理:缓冲字节输入流自带了8KB缓冲池;缓冲字节输出流也自带了8KB缓冲池。
- 功能上并无很大变化,性能提升了。
| 构造器 |
说明 |
| public BufferedInputStream(InputStream is) |
把低级的字节输入流包装成一个高级的缓冲字节输入流,从而提高读数据性能 |
| public BufferedOutputStream(OutputStream os) |
把低级的字节输出流包装成一个高级的缓冲字节输出流,提升写数据性能 |
点击查看代码
public static void copyFile(String source, String destination) {
try(// 创建一个文件输入流
InputStream is=new FileInputStream(source);
// 创建一个缓冲输入流,把文件输入流放到缓冲输入流中
InputStream bis=new BufferedInputStream(is);
// 创建一个文件输出流
OutputStream os=new FileOutputStream(destination,true);
// 创建一个缓冲输出流,把文件输出流放到缓冲输出流中
OutputStream bos=new BufferedOutputStream(os);
){
byte[] bytes=new byte[1024];
int len;
while ((len=bis.read(bytes))!=-1){//使用缓冲字节输入流读取数据
bos.write(bytes,0,len);//使用缓冲字节输出流写出数据
}
bos.flush();
System.out.println("文件复制成功");
}catch (Exception e){throw new RuntimeException(e);}
}
二)缓冲字符流
1.两种字符缓冲流如何使用(包装)?
public BufferedReader(Reader r)
public BufferedWriter(Writer w)
- 作用:自带8K(8192)的字符缓冲池,可以提高字符输入流读取字符数据的性能和字符输出流写字符数据的性能。
| 构造器 |
说明 |
| public BufferedReader(Reader r) |
把低级的字符输入流包装成高级的字符缓冲输入流管道,从而提高字符输入流读字符数据的性能 |
| public BufferedWriter(Writer r) |
把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能 |
- 字符缓冲输入流新增的功能:按照行读取字符
| 方法 |
说明 |
| public String readLine() |
读取一行数据返回,如果没有数据可读了,会返回null |
- 字符缓冲输出流新增的功能:换行
| 方法 |
说明 |
| public void newLine() |
换行 |
点击查看代码
public static void bfRW(){
try(BufferedReader br = new BufferedReader(new FileReader("resource/hello.txt"));//创建一个文件缓冲字符输入流
BufferedWriter bw = new BufferedWriter(new FileWriter("resource/hello2.txt"))//创建一个文件缓冲字符输出流
){
String line = null;
while ((line = br.readLine())!=null){//Buffered.readLine()读取一行为独有的换行功能,不用多态写法
System.out.println(line);
// bw.write(line);//复制语句
// bw.newLine();//换行功能
}
//目前读取文本最优方案:性能好,不乱码,可以按照行读取。
}catch (Exception e){throw new RuntimeException(e);}
}
Ⅴ.性能分析
- 默认桶大小设置为8KB,过大没有用,过小影响性能。当低级流读写数据时,桶加大可以媲美高级池。
- 建议使用字节缓冲输入流、字节缓冲输出流,结合字节数组的方式,目前看来是性能最优的组合。
点击查看代码
public class TimeTest2 {
private static final String SRC_FILE="C:\\Users\\20984\\Pictures\\Screenshots\\屏幕截图 2025-06-26 191214.png";
private static final String DEST_FILE="D:\\";
public static void main(String[] args){
//使用低级的字节流按照一个一个字节的形式复制文件:非常慢,直接淘汰!!
copyFile1();
//使用低级的字节流按照字节数组的形式复制文件:比较慢,还可以接受。
copyFile2();
//使用高级的字节流按照一个一个字节形式复制文件:虽然是高级管道,但一个一个字节的复制还是太慢了,直接淘汰!
copyFile3();
//使用高级的字节流按照字节数组的形式复制文件:非常快!推荐使用!
copyFile4();
}
public static void copyFile4() {
//拿系统当前时间
long start=System.currentTimeMillis();
try (
InputStream fis=new FileInputStream(SRC_FILE);
InputStream bis=new BufferedInputStream(fis);
OutputStream fos=new FileOutputStream(DEST_FILE+"4.png");
OutputStream bos=new BufferedOutputStream(fos);
){
byte[] bytes=new byte[1024];//1KB
int len;
while((len=fis.read(bytes))!=-1){}
}catch(Exception e){
e.printStackTrace();
}
long end=System.currentTimeMillis();
System.out.println("高级字节流按照字节数组的形式复制文件,耗时:"+(end-start)+"毫秒");
}
public static void copyFile3() {
//拿系统当前时间
long start=System.currentTimeMillis();
try (
InputStream fis=new FileInputStream(SRC_FILE);
InputStream bis=new BufferedInputStream(fis);
OutputStream fos=new FileOutputStream(DEST_FILE+"3.png");
OutputStream bos=new BufferedOutputStream(fos);
){
int b;
while((b=fis.read())!=-1){
fos.write(b);
}
}catch(Exception e){
e.printStackTrace();
}
long end=System.currentTimeMillis();
System.out.println("高级字节流按照一个一个字节形式复制文件,耗时:"+(end-start)+"毫秒");
}
public static void copyFile2() {
//拿系统当前时间
long start=System.currentTimeMillis();
try (
InputStream fis=new FileInputStream(SRC_FILE);
OutputStream fos=new FileOutputStream(DEST_FILE+"2.png");
){
byte[] bytes=new byte[1024];//1KB
int len;
while((len=fis.read(bytes))!=-1){
fos.write(bytes,0,len);
}
}catch (Exception e){
e.printStackTrace();
}
long end=System.currentTimeMillis();
System.out.println("低级字节流按照字节数组的形式复制文件,耗时:"+(end-start)+"毫秒");
}
public static void copyFile1() {
//拿系统当前时间
long start=System.currentTimeMillis();//此刻时间毫秒值:从1970-1-1 00:00:00开始走到此刻的总毫秒值 1s=1000ms
try (
InputStream fis=new FileInputStream(SRC_FILE);
OutputStream fos=new FileOutputStream(DEST_FILE+"1.png");
){
int b;
while((b=fis.read())!=-1){
fos.write(b);
}
}catch(Exception e){
e.printStackTrace();
}
long end=System.currentTimeMillis();//此刻时间毫秒值:从1970-1-1 00:00:00开始走到此刻的总毫秒值 1s=1000ms
System.out.println("低级字节流按照一个一个字节的形式复制文件,耗时:"+(end-start)+"毫秒");
}
}
Ⅵ.其他流
一)字符输入转换流
- InputStreamReader(字符输入转换流)继承于Reader(字符输入流)。
- 作用:用于解决不同编码时,字符流读取文本内容乱码的问题。
- 解决思路:先获取文件的原始字节流,再将其按照真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了。
| 构造器 |
说明 |
| public InputStreamReader(InputStream is) |
把原始的字节输入流,按照代码默认编码转成字符输入流(与直接使用FileReader的效果一样) |
| public InputStreamReader(InputStream is,String charset) |
把原始字节输入流,按照指定字符集编码转成字符输入流(重点) |
InputStreamReader isr=new InputStreamReader(new FileInputStream("resource/hello.txt"),"GBK");
BufferedReader br=new BufferedReader(isr);//缓冲字符输入流
二)打印流
- PrintStream/PrintWriter(打印流)分别继承于OutputStream(字节输出流)和Writer(字符输出流)。
- 作用:打印流可以实现更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去。
- PrintStream提供的打印数据的方案
| 构造器 |
说明 |
| public PrintStream(OutputStream/File/String) |
打印流直接通向字节输出流/文件/文件路径 |
| public PrintStream(String fileName, Charset charset) |
可以指定写出去的字符编码 |
| public PrintStream(OutputStream out, boolean autoFlush) |
可以指定实现自动刷新 |
| public PrintStream(OutputStream out, boolean autoFlush, String encoding) |
可以指定实现自动刷新,并可指定字符的编码 |
| 方法 |
说明 |
| public void print(Xxx xx) |
打印任意类型的数据出去,不换行 |
| public void println(Xxx xx) |
打印任意类型的数据出去,换行 |
| public void write(int/byte[]/byte[]一部分) |
可以写字节数据出去(一般不用) |
- 打印流一般是指:PrintStream、PrintWriter两个类。
- 打印功能两者是一样的使用方式。
- PrintStream和PrintWriter的区别
(1)打印数据的功能上是一模一样的:都是使用方便,性能高效(核心优势)。
(2)PrintStream继承自字节输出流OutputStream,因此支持写字节数据的方法。
(3)PrintWriter继承自字符输出流Writer,因此支持写字符数据出去。
点击查看代码
public static void printstream(){
try(
PrintStream ps = new PrintStream(new FileOutputStream("resource/hello.txt",true));
//不能在高级管道追加,只能在低级管道追加
PrintWriter pw = new PrintWriter(new FileWriter("resource/hello.txt",true));
){
ps.println("hello world");
ps.println(97);
ps.println('a');
ps.println("你好");
ps.println(true);
ps.println(99.9);
}
catch(Exception e){
e.printStackTrace();
}
}
三)特殊数据流(通信)
- DataOutputStream(数据输出流):允许把数据和其类型一并写出去。
| 构造器 |
说明 |
| public DataoutputStream(OutputStream os) |
创建新的数据输出流包装基础的字节输出流 |
| 方法 |
说明 |
| public final void writeByte(int v) throws IOException |
将byte类型的数据写入基础的字节输出流 |
| public final void writeInt(int v) throws IOException |
将int类型的数据写入基础的字节输出流 |
| public final void writeDouble(Double v) throws IOException |
将double类型的数据写入基础的字节输出流 |
| public final void writeUTF(String str) throws IOException |
将字符串数据以UTF-8编码成字节写入基础的字节输出流 |
| public void write(int/byte[ ]/byte[ ]一部分) |
支持写字节数据出去(一般不用) |
- DataInputStream(数据输入流):用于读取数据输出流写出去的数据。
| 构造器 |
说明 |
| public DataInputStream(InputStream is) |
创建新的数据输入流包装基础的字节输入流 |
| 方法 |
说明 |
| public final byte readByte() throws IOException |
读取字节数据返回 |
| public final int readInt() throws IOException |
读取int类型的数据返回 |
| public final double readDouble() throws IOException |
读取double类型的数据返回 |
| public final String readUTF() throws IOException |
读取字符串数据(UTF-8)返回 |
| public int readInt()/read(byte[ ]) |
支持读字节数据进来(一般不用) |
- 打印与读取都要一一对应,不要跳读,不然会出错。
四)总结-IO框架
- 什么是框架?
(1)框架(Framework)是一个预先写好的代码库或一组工具,旨在简化和加速开发过程。
(2)框架的形式:一般是把类、接口等编译成class形式,再压缩成一个.jar结尾的文件发行出去。
- IO框架:封装了Java提供的对文件、数据进行操作的代码,对外提供了更简单的方式来对文件进行操作,对数据进行读写等。
- 导入commons-io-2.20.0.jar框架到项目中去。
(1)在项目中创建一个文件夹:lib
(2)将commons-io-2.20.0.jar文件复制到lib文件夹
(3)在jar文件上点右键,选择Add as Library->点击OK
(4)在类中导包使用
- Commons-io框架
- Commons-io是apache开源基金组织提供的一组有关IO操作的小框架,目的是提高IO流的开发效率。
| FileUtils类提供的部分方法展示 |
说明 |
| public static void copyFile(File srcFile,File destFile) |
复制文件 |
| public static void copyDirectory(File srcDir,File destDir) |
复制文件夹 |
| public static void deleteDirectory(File directory) |
删除文件夹 |
| public static String readFileToString(File file,String encoding) |
读数据 |
| public static void writeStringToFile(File file,String data,String charname,boolean append) |
写数据 |
| public static void forceDelete(File file) |
删除文件,如果文件不存在,则不抛异常 |
| public static void forceMkdir(File directory) |
创建文件夹,如果文件夹已经存在,则不抛异常 |
| public static void writeStringToFile(File file,String data,String charname,boolean append) |
把字符串写入文件 |
| IOUtils类提供的部分方法展示 |
说明 |
| public static int copy(InputStream inputStream,OutputStream outputStream) |
复制文件 |
| public static int copy(Reader reader,Writer writer) |
复制文件 |
| public static void write(String data,OutputStream output,String charsetName) |
写数据 |
点击查看代码
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
public class CommonsIoDemo1 {
public static void main(String[] args) throws Exception{
//IO框架
FileUtils.copyFile(new File("day03-file-io\\src\\come\\itheima\\demo11bufferedWriter\\csb_copy.txt"),new File("day03-file-io\\src\\csb_copy2.txt"));
//JDK 7提供的
//Files.copy(Path.of("day03-file-io\\src\\come\\itheima\\demo11bufferedWriter\\csb_copy.txt"),Path.of("day03-file-io\\src\\csb_copy3.txt"));
//删除文件夹,包括文件夹中所有文件
FileUtils.deleteDirectory(new File("D:\\新建文件夹"));
}
}
(七)线程
- 线程(Thread)是一个程序内部的一条执行流程。
- 程序中如果只有一条执行流程,那这个程序就是单线程的程序。
Ⅰ.多线程
- 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
一)创建方式一:继承Thread类
- 多线程的创建方式一:
(1)定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法。
(2)创建MyThread类的对象。
(3)调用线程对象的start方法(p.start();)启动线程(启动后还是执行run方法的)。
- 方式一优缺点:
(1)优点:编码简单
(2)缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
- 创建线程的注意事项
(1)启动线程必须时调用start方法,不是调用run方法。
① 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
② 只有调用start方法才是启动一个新的线程执行。
(2)不要把主线程任务放在启动子线程之前。
-- 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
点击查看代码
public class threadstest {
//main方法本身是由一条主线程负责执行的
public static void main(String[] args) {
MyThread t=new MyThread();//4、创建一个线程对象,才能代表线程
t.start();//5、启动线程,向CPU注册请求开启一个线程
//t线程执行,在主线程外执行,必须在t.start()后面才能开启t线程
for (int i=0;i<5;i++) System.out.println("main is running: "+i);
/**输出:(每次情况都不一样,主线程和MyThread线程并行)
* main is running: 0
* main is running: 1
* MyThread is running: 0
* main is running: 2
* main is running: 3
* main is running: 4
* MyThread is running: 1
* MyThread is running: 2
* MyThread is running: 3
* MyThread is running: 4
*/
}
}
class MyThread extends Thread{//1、定义定义一个子类继承Thread类,成为线程类
//2、重写Threads类的run方法
@Override
public void run(){
for (int i=0;i<5;i++)System.out.println("MyThread is running: "+i);
//3、在run方法中编写线程的任务代码(线程的任务)
}
}
二)创建方式二:实现Runnable接口
- 多线程的创建方式二:
(1)定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
(2)创建MyRunnable任务对象
(3)把MyRunnable任务对象交给Thread处理new Thread(new MyRunnable());
(4)调用线程对象的start()方法启动线程
| Thread类提供的构造器 |
说明 |
| public Thread(Runnable target) |
封装Runnable对象成为线程对象 |
- 匿名内部类写法
(1)可以创建Runnable的匿名内部类对象。
(2)再交给Thread线程对象。
(3)在调用线程对象的start()启动线程。
- 方式二的优缺点:
(1)优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
(2)缺点:需要多一个Runnable对象。
点击查看代码
public class runnable {
public static void main(String[] args) {
//3、创建线程任务对象代表一个线程任务
Runnable r=new MyRunnable();
//4、把线程任务对象交给一个线程对象来处理
Thread t=new Thread(r);
//5、启动线程
t.start();
for (int i=0;i<5;i++) System.out.println("main is running: "+i);
//匿名内部类实现:
new Thread(new Runnable() {
@Override
public void run() {for (int i=0;i<5;i++) System.out.println("MyThread is running: "+i);}
}).start();//直接调用start()启动
//函数式接口匿名内部类
new Thread(()->{for (int i=0;i<5;i++) System.out.println("MyThread is running: "+i);}).start();
}
}
//1、定义一个线程任务类实现Runnable接口
class MyRunnable implements Runnable {
//2、重写run方法,设置线程任务
@Override
public void run(){
for (int i=0;i<5;i++)System.out.println("MyThread is running: "+i);
}
}
三)创建方式三:实现Callable接口
- 前两种线程创建方式,假如线程执行完后有一些数据需要返回,它们重写的run()方法均不能直接返回结果,因为run的返回值为void。
- JDK5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式),该方式可以返回线程执行完毕后的结果。
- 多线程的创建方式三:利用Callable接口、FutureTask类来实现
(1)创建任务对象
① 定义一个类实现Callable接口,重写call()方法,封装要做的事情,和要返回的数据。
② 把Callable类型的对象封装成FutureTask(线程任务对象)。
(2)把线程任务对象交给Thread对象。
(3)调用Thread对象的start方法启动线程。
(4)线程执行完毕后,通过FutyreTask对象的get方法去获取线程任务执行的结果。
- FutureTask的API
| FutureTask提供的构造器 |
说明 |
| public FutureTask<>(Callable call) |
把Callable对象封装成FutureTask对象 |
| FutureTask提供的方法 |
说明 |
| public V get() throws Exception |
获取线程执行call方法返回的结果 |
- 线程创建方式三的优缺点
(1)优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
(2)缺点:编码复杂。
点击查看代码
public class callable {
public static void main(String[] args) {
//3、创建一个callable接口的实现类对象
MyCallable c=new MyCallable(100);
//4、把callable对象封装成一个线程任务对象FutureTask对象
/**
* FutureTask类实现了Runnable接口,所以可以作为线程任务对象
* 线程任务对象可以作为线程对象来启动,可以获取线程执行完毕后的结果
*/
Runnable t=new FutureTask<Integer>(c);
//5、把FutureTask对象交给一个线程对象来处理
Thread thread=new Thread(t);
thread.start();
//6、调用FutureTask对象的get方法,获取线程执行完毕后的结果
try {
System.out.println(thread.getName()+" is running: "+((FutureTask<Integer>) t).get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//1、定义实现类,实现callable接口
class MyCallable implements Callable<Integer> {
private int sum;
public MyCallable(int sum) {this.sum=sum;}
//2、重写call方法,返回计算结果
public Integer call() throws Exception {
int n = 0;
for (int i = 1; i <= sum; i++) {
n += i;
}
return n;
}
}
四)常用方法
- Thread的常用方法
| Thread提供的常用方法 |
说明 |
| public void run() |
线程任务方法,线程执行体,线程被创建后,必须重写run方法 |
| public void start() |
启动线程,向CPU注册请求开启一个线程 |
| public void String getName() |
获取当前线程的名称,线程名称默认是Thread-索引 |
| public void setName(String name) |
为线程设置名称,默认是Thread-索引,注意要放在启动线程之前 |
| public static Thread currentThread() |
获取当前执行的线程对象,当前线程对象是静态方法,所以可以直接通过类名调用 |
| public static void sleep(long time) |
让当前执行的线程休眠多少毫秒后,再继续执行 |
| public final void join()... |
让调用当前的这个方法的线程先执行完! |
| Thread提供的常见构造器 |
说明 |
| public Thread(String name) |
为当前线程对象指定名称 |
| public Thread(Runnable target) |
封装Runnable对象成为线程对象 |
| public Thread(Runnable target, String name) |
封装Runnable对象成为线程对象,并指定线程名称 |
Ⅱ.线程安全
- 线程安全问题:多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
点击查看代码
public class threadsafe {
public static void main(String[] args) {
//创建一个账户类,用于创建A,B共同账户对象,存入100。
Account A=new Account(1,100);
//模拟两个线程,A和B,A和B共同操作同一个账户,A和B同时取钱,每次取10。
new ABThread("A",A).start();
new ABThread("B",A).start();
}
}
class Account {
private Integer id;
private double balance;
public void getMoney(double balance){
String name=Thread.currentThread().getName();
if(this.balance>=balance){
System.out.println(name+"取钱成功,余额为:"+this.balance);
this.balance-=balance;
System.out.println(name+"取钱成功,余额应该为:"+this.balance);
}else{
System.out.println(name+"取钱失败,余额不足,余额为:"+this.balance);
}
}
//省略getter和setter方法,有参无参构造器,toString方法
}
class ABThread extends Thread {
private Account account;
public ABThread(String id,Account account){
super(id);
this.account=account;
}
@Override
public void run() {
account.getMoney(100);
}
}
Ⅲ.线程同步
- 线程同步:线程同步是线程安全问题的解决方案。
- 线程同步的核心思想:让多个线程先后依次访问共享资源,避免多线程操作共享资源时出现线程安全问题。
- 线程同步常见方案
-- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
- 方式一:同步代码块
(1)作用:把访问共享资源的核心代码给上锁,以此保证线程安全。synchronized(对于线程来说是唯一的对象(同步锁)){访问共享资源的核心代码}
(2)原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
(3)注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
(4)锁对象不推荐随便使用一个唯一的对象,会影响其他无关线程的执行。
(5)锁对象的使用规范:
① 建议使用共享资源作为锁对象,对于实例方法用this作为锁对象。
② 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
点击查看代码
public class threadsafe {
public static void main(String[] args) {
//创建一个账户类,用于创建A,B共同账户对象,存入100。
Account A=new Account(1,100);
//模拟两个线程,A和B,A和B共同操作同一个账户,A和B同时取钱,每次取10。
new ABThread("A",A).start();
new ABThread("B",A).start();
}
}
class Account {
private Integer id;
private double balance;
public void getMoney(double balance){
String name=Thread.currentThread().getName();
// synchronized ("hei"){
// 同步锁只能为一个唯一对象,这里使用字符串,字符串是内存内只能加载一份,所以可以作为锁。
//同步锁只能为一个对象,这里使用this,this为当前对象,不用像字符串一样,每次锁的时候影响到其他对象
synchronized (this){
if(this.balance>=balance){
System.out.println(name+"取钱成功,余额为:"+this.balance);
this.balance-=balance;
System.out.println(name+"取钱成功,余额应该为:"+this.balance);
}else{
System.out.println(name+"取钱失败,余额不足,余额为:"+this.balance);
}
}//锁对象:对于实例方法用this作为锁,对于静态方法用类名.class作为锁。
}
//省略getter和setter方法,有参无参构造器,toString方法
}
class ABThread extends Thread {
private Account account;
public ABThread(String id,Account account){
super(id);
this.account=account;
}
@Override
public void run() {
account.getMoney(100);
}
}
- 方式二:同步方法
(1)作用:把访问共享资源的核心方法给上锁以此保证线程安全。修饰符 synchronized 返回值类型 方法名(形参列表){操作共享资源的核心代码}
(2)原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
(3)同步方法底层原理
① 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法去代码。
② 如果方法是实例方法:同步方法默认用this作为锁对象。
③ 如果方法是静态方法:同步方法默认用类名.class作为锁对象。
点击查看代码
public class threadsafe {
public static void main(String[] args) {
//创建一个账户类,用于创建A,B共同账户对象,存入100。
Account A=new Account(1,100);
//模拟两个线程,A和B,A和B共同操作同一个账户,A和B同时取钱,每次取10。
new ABThread("A",A).start();
new ABThread("B",A).start();
}
}
class Account {
private Integer id;
private double balance;
public synchronized void getMoney(double balance){//把核心方法给上锁
String name=Thread.currentThread().getName();
if(this.balance>=balance){
System.out.println(name+"取钱成功,余额为:"+this.balance);
this.balance-=balance;
System.out.println(name+"取钱成功,余额应该为:"+this.balance);
}else{
System.out.println(name+"取钱失败,余额不足,余额为:"+this.balance);
}
}
//省略getter和setter方法,有参无参构造器,toString方法
}
class ABThread extends Thread {
private Account account;
public ABThread(String id,Account account){
super(id);
this.account=account;
}
@Override
public void run() {
account.getMoney(100);
}
}
- 同步代码块与同步方法相比:
(1)范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
(2)可读性:同步方法更好。
- 方式三:Lock锁
(1)Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更加灵活、更方便、更强大。
(2)Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
(3)锁对象建议加上final修饰,防止被别人篡改。
(4)建议将释放锁的操作放到finally代码块中,确保锁用完了一定会被释放。
| 构造器 |
说明 |
| public ReentrantLock() |
获得Lock锁的实现类对象 |
Lock的常用方法
| 方法名称 |
说明 |
| void lock() |
获得锁 |
| void unlock() |
释放锁 |
点击查看代码
//省略main类
class Account {
private Integer id;
private double balance;
private final Lock lock=new ReentrantLock();//一个账户一把锁,加final表示不能修改
//创建一个锁对象,不能为静态,静态的话所有对象都用一把锁。
public void getMoney(double balance){
String name=Thread.currentThread().getName();
lock.lock();//加锁
try {
if(this.balance>=balance){
System.out.println(name+"取钱成功,余额为:"+this.balance);
this.balance-=balance;
System.out.println(name+"取钱成功,余额应该为:"+this.balance);
}else{
System.out.println(name+"取钱失败,余额不足,余额为:"+this.balance);
}
}finally {
lock.unlock();//解锁需要放在finally中,否则可能造成死锁。
}
}
}
Ⅳ.线程池
- 线程池:线程池就是一个可以复用线程的技术。
- 不使用线程池的问题:用户每发起一个请求,后台就需要创建一个新的线程来处理,下一次新任务来了肯定又要创建新线程处理的,创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
创建线程池
- JDK5.0起提供了代表线程池的接口:ExecutorService(执行服务)。
- 如何创建线程池对象?
(1)方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
(2)方法二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
- 方式一:通过使用ThreadPoolExecutor创建线程池
| ThreadPoolExecutor类提供的构造器 |
作用 |
| public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectionExecutionHandler handler) |
使用指定的初始化参数创建一个新的线程池对象 |
- 参数一:corePoolSize:指定线程池的核心线程的数量。(正式工:3)
- 参数二:maximumPoolSize:指定线程池的最大线程数量。(最大员工数:5 临时工:2)
- 参数三:keepAliveTime:指定临时线程的存活时间。(临时工空闲多久被开除)
- 参数四:unit:keepAliveTime的单位,默认为毫秒,指定临时线程存活的时间单位(秒、分、时、天)
- 参数五:workQueue:指定线程池的任务队列(客人排队的地方),可以基于数组(new ArrayBlockingQueue)或链表(new LinkedBlockingDeque)的任务队列,一般用ArrayBlockingQueue,可以指定容量。
- 参数六:threadFactory:指定线程池的线程工厂(负责招聘员工的(hr))。
- 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)(忙不过来咋办)。
CPU密集型任务(计算密集型任务),线程池的常驻线程数量为:
int maxnumPoolSize = Runtime.getRuntime().availableProcessors() + 1;//或等于corePoolSize,根据具体需求调整
IO密集型任务(网络IO密集型任务),线程池的常驻线程数量为:
double blockingFactor=0.9;//阻塞系数,例如90%的时间在等待IO
int maxnumPoolSize = ((int) Math.ceil(Runtime.getRuntime().availableProcessors() /(1-block)))* 2;//或者更高,根据具体需求调整
核心线程数=CPU核数(1/(1-阻塞系数))
最大线程数=核心线程数倍速(根据具体需求调整,例如2倍)
ExecutorService的常用方法
| 方法名称 |
说明 |
| void execute(Runnable command) |
执行Runnable任务 |
| Future submit(Callable task) |
执行Callable任务,返回未来任务对象,用于获取线程返回的结果 |
| void shutdown() |
等全部任务执行完毕后,再关闭线程池! |
| List shutdownNow() |
立即关闭线程池,停止正在执行的任务,并返回队列中为执行的任务 |
处理Runnable任务代码
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
//创建线程池对象,使用线程池的实现类
//1.使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。
ExecutorService pool=new ThreadPoolExecutor(3, 5,
10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//2.使用线程池执行Runnable任务
Runnable target=new MyRunnable();
pool.execute(target);//任务可以复用,提交的第一个线程,自动启动线程处理这个任务
pool.execute(target);//提交的第二个线程,自动启动线程处理这个任务
pool.execute(target);//提交的第三个线程,自动启动线程处理这个任务
pool.execute(target);//复用线程
pool.execute(target);//复用线程
//3.关闭线程池:一般不关闭线程池。
//pool.shutdown();//等所有任务执行完毕后关闭线程池
//pool.shutdownNow();//立即关闭线程池,不管任务是否执行完毕。
}
}
class MyRunnable implements Runnable{
//重写run方法,设置线程任务
@Override
public void run(){
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"正在执行任务:"+i);
}
}
}
处理Callable任务代码
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
//创建线程池对象,使用线程池的实现类
//1.使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。
ExecutorService pool=new ThreadPoolExecutor(3, 5,
10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//使用线程池执行Callable任务
Future<String> f1=pool.submit(new MyCallable(100));
Future<String> f2=pool.submit(new MyCallable(200));
Future<String> f3=pool.submit(new MyCallable(300));
Future<String> f4=pool.submit(new MyCallable(400));
try {
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {this.n = n;}
@Override
public String call() throws Exception {
int sum=0;
for (int i=1;i<=n;i++) {
// System.out.println(Thread.currentThread().getName()+" is running:"+i);
sum+=i;
}
return Thread.currentThread().getName()+"计算1-"+n+"的和是:"+sum;
}
}
- 线程池的注意事项
(1)开始创建临时线程:新任务提交时发现核心线程在忙,任务队列也满了,并且还可以创建临时线程时,此时才会创建临时线程。
(2)开始拒绝新任务(handler):核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
(3)任务拒绝策略
| 策略 |
说明 |
| ThreadPoolExecutor.AbortPolicy() |
丢弃任务并抛出RejectedExecutionException异常。(是默认的策略) |
| ThreadPoolExecutor.DiscardPolicy() |
丢弃任务,但是不抛出异常,这是不推荐的做法 |
| ThreadPoolExecutor.DiscardOldestPolicy() |
抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
| ThreadPoolExecutor.CallerRunsPolicy() |
由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
点击查看代码
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
//创建线程池对象,使用线程池的实现类
//1.使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。
ExecutorService pool=new ThreadPoolExecutor(3, 5,
10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//2.使用线程池执行Runnable任务
Runnable target=new MyRunnable();
pool.execute(target);//任务可以复用,提交的第一个线程,自动启动线程处理这个任务
pool.execute(target);//提交的第二个线程,自动启动线程处理这个任务
pool.execute(target);//提交的第三个线程,自动启动线程处理这个任务
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);//到了临界线程的创建时机了
pool.execute(target);//到达了临界线程的创建时机了
pool.execute(target);//到了任务拒绝策略了,忙不过来
//3.关闭线程池:一般不关闭线程池。
//pool.shutdown();//等所有任务执行完毕后关闭线程池
//pool.shutdownNow();//立即关闭线程池,不管任务是否执行完毕。
}
}
class MyRunnable implements Runnable{
//重写run方法,设置线程任务
@Override
public void run(){
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"正在执行任务:"+i);
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 方式二:通过Executors工具类创建线程池
- 是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
| 方法名称 |
说明 |
| public static ExecutorService newFixedThreadPool(int nThreads) |
创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
| public static ExecutorService newSingleThreadExecutor() |
创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
| public static ExecutorService newCachedThreadPool() |
线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。 |
| public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
- 注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
- 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
Ⅴ.并发/并行
- 进程:
(1)正在运行的程序(软件)就是一个独立的进程。
(2)线程是属于进程的,一个进程中可以同时运行很多个线程。
(3)进程中的多个线程其实是是并发和并行执行的。
- 并发的含义:进程中的线程是由CPU负责调度执行的,但CPU能同时及处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
- 并行的理解:在同一个时刻上,同时有多个线程被CPU调度执行。
(八)网络编程
- 可以让设备中的程序与网络上其他设备中的程序进行数据交换的技术(实现网络通信)。
Ⅰ.基本的通信架构
- 基本的通信架构有两种形式:CS架构(Client客户端/Server服务端)、BS架构(Browser浏览器/Server服务端)。
- 无论是CS架构,还是BS架构的软件都必须依赖网络编程!
- java.net.*包下提供了网络编程的解决方案!
Ⅱ.网络编程三要素
- IP地址:设备在网络中的地址,是设备在网络中的唯一标识。
- 端口:应用程序在设备中的唯一标识。
- 协议:连接和数据在网络中传输的规矩。
一)IP地址
- IP(Internet Protocol):全称“互联网协议地址”,是分配给上网设备的唯一标识。
- 目前,被广泛采用的IP地址形式有两种:IPv4、IPv6.
- IPv4
IPv4是Internet Protocol version 4的缩写,它使用32位地址,通常以点分十进制表示。
- IPv6
(1)IPv6是Internet Protocol version 6的缩写,它使用128位地址,号称可以为地球上的每一粒沙子编号。
(2)IPv6分成8段,每段每四位编码成一个十六进制位表示,每段之间用冒号(:)分开,将这种方式称为冒分十六进制。
- IP域名(Domain Name)
(1)用于在互联网上识别和定位网站的人类可读的名称。(例如:www.baidu.com/www.itheima.com)
(2)DNS域名解析(Domain Name System):是互联网中用于将域名转换为对应IP地址的分布式命名系统。它充当了互联网的“电话簿”,将易记的域名映射到数字化的IP地址,使得用户可以通过域名来访问网站和其他网络资源。
- 公网IP、内网IP
(1)公网IP:是可以连接到互联网的IP地址;
(2)内网IP:也叫局域网IP,是只能组织机构内部使用的IP地址:例如,192.168.开头的就是常见的局域网地址,范围为192.168.0.0--192.255.255,专门为组织机构内部使用。
- 本机IP:127.0.0.1、localhost:代表本机IP,只会寻找当前程序所在的主机。
- IP常见命令
(1)ipconfig:查看本机IP地址。
(2)ping IP地址:检查网络是否连通。
- InetAddress
代表IP地址
InetAddress的常用方法
| InetAddress类常用方法 |
说明 |
| public static InetAddress getLocalHost() throw UnknownHostException |
获取本机IP,返回一个InetAddress对象 |
| public String getHostName() |
获取该ip地址对象对应的主机名。 |
| public String getHostAddress() |
获取该ip地址对象中的IP地址信息。 |
| public static InetAddress getByName(String host) throws UnknownHostException |
根据ip地址或者域名,返回一个InetAddress对象 |
| public boolean isReachable(int timeout) throws IOException |
判断主机在指定毫秒内与该ip对应的主机是否能连通 |
点击查看代码
import java.net.InetAddress;
public class InetAddressDemo1 {
public static void main(String[] args){
try {
//1.获取本机IP对象
InetAddress ip1=InetAddress.getLocalHost();
System.out.println(ip1);
System.out.println(ip1.getHostName());
System.out.println(ip1.getHostAddress());
//2.获取对方IP对象
InetAddress ip2=InetAddress.getByName("www.baidu.com");
System.out.println(ip2);
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());
//3.判断本机与对付主机是否互通
System.out.println(ip1.isReachable(5000));//与ping命令相似
} catch (Exception e) {
e.printStackTrace();
}
}
}
二)端口
- 用来标记正在计算机设备上运行的应用程序,被规定为一个16位的二进制,范围是0~65535。
- 端口分类
(1)周知端口:0~1023被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)。
(2)注册端口:1024~49151,分配给用户进程或某些应用程序。
(3)动态端口:49152~65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
- 注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则报错。
三)协议
- 通信协议:网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。
- 开放式网络互联标准:OSI网络参考模型(全球互联网标准)
(1)OSI网络参考模型:全球网络互联标准(理论上)。
(2)TCP/IP网络模型:事实上的国际标准。
- 传输层的2个通信协议
(1)UDP:用户数据报协议。
(2)TCP:传输控制协议。
- UDP协议
(1)特点:无连接、不可靠通信、通信效率高。
(2)不事先建立联系,数据按照包发,一包数据包含:自己的IP、自己的端口、目的地IP、目的端口和数据(限制在64KB内) 等。
(3)发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的。
(4)速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据。
- TCP协议
(1)特点:面向连接、可靠通信。
(2)TCP的最终目的:要保证在不可靠的信道上实现可靠的数据传输。
(3)TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。
(4)三次握手建立可靠连接——可靠连接:确保通信双方发消息没有问题(全双工)(C-发出请求->S-返回一个响应->C-再次发出确认信息,建立联系->S)
(5)四次挥手断开连接——确保通信双方收发消息都已经完成(C-发出断开连接请求->S-返回一个响应:稍等->C,S-返回一个相应:消息处理完毕确认断开->C-发出确认断开信息连接断开->S)
四)UDP通信
- UDP通信的实现
(1)特点:无连接、不可靠通信。
(2)不事先建立连接,发送端每次把要发送的数据(<64KB)、接收端IP等信息封装成一个数据包,发出去就不管了。
(3)java提供了一个java.net.DatagramSocket类来实现UDP通信。
DagramSocket:用于创建客户端、服务端
| 构造器 |
说明 |
| public DatagramSocket() |
创建客户端的Socket对象,系统随机分配一个端口号。 |
| public DatagramSocket(int port) |
创建服务端的Socket对象,指定端口号 |
| 方法 |
说明 |
| public void send(DatagramPacket dp) |
发送数据包 |
| public void receive(DatagramPacket p) |
使用数据包接收数据 |
DatagramPacket:创建数据包
| 构造器 |
说明 |
| public DatagramPacket(byte[] buf, int length,InetAddress address,int port) |
创建发出去的数据包对象 |
| public DatagramPacket(byte[] buf, int length) |
创建用来接收数据的数据包 |
单发单收
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClientDemo1 {
public static void main(String[] args) throws Exception{
System.out.println("==客户端启动了==");
//1.创建发送端对象
DatagramSocket socket=new DatagramSocket();//随机端口
//2.创建数据包对象封装要发送的数据。
byte[] bys="我是客户端".getBytes();
/**
* public DatagramPacket(byte[] buf, int length,InetAddress address,int port)
* 参数一:发送的数据,字节数组。
* 参数二:发送字节数组的长度。
* 参数三:目的地的IP地址。
* 参数四:服务端程序的端口号。
*/
DatagramPacket packet=new DatagramPacket(bys,bys.length, InetAddress.getLocalHost(),8080);
//3.让发送端对象发送数据包的数据
socket.send(packet);
}
}
class UDPServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("==服务端启动了==");
//1.创建接收端对象,注册端口。
DatagramSocket socket=new DatagramSocket(8080);
//2.创建一个数据包对象负责接收数据。
byte[] buf=new byte[1024*64];
DatagramPacket packet=new DatagramPacket(buf,buf.length);
//3.接收数据,将数据封装到数据包对象的字节数组中去
socket.receive(packet);
//4.看看数据是否收到了
int len=packet.getLength(); //获取当前收到的数据长度。
String data=new String(buf,0,len);
System.out.println("服务端收到了:"+data);
//获取对方的IP对象和程序端口
String ip=packet.getAddress().getHostAddress(); //获取对方的IP
int port=packet.getPort(); //获取对方程序端口
System.out.println("对方IP:"+ip+" 对方端口:"+ port);
}
}
多发多收
public class UDPClientDemo1 {
public static void main(String[] args) throws Exception{
System.out.println("==客户端启动了==");
//1.创建发送端对象
DatagramSocket socket=new DatagramSocket();//随机端口
Scanner sc=new Scanner(System.in);
while(true) {
System.out.println("请输入要发送的数据:");
String msg = sc.nextLine();
//如果用户输入的是exit,则退出
if ("exit".equals(msg)) {
System.out.println("==客户端退出了==");
socket.close();
break;
}
//2.创建数据包对象封装要发送的数据。
byte[] bys = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bys, bys.length, InetAddress.getLocalHost(), 8080);
//3.让发送端对象发送数据包的数据
socket.send(packet);
}
}
}
public class UDPServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("==服务端启动了==");
//1.创建接收端对象,注册端口。
DatagramSocket socket=new DatagramSocket(8080);
//2.创建一个数据包对象负责接收数据。
byte[] buf=new byte[1024*64];
DatagramPacket packet=new DatagramPacket(buf,buf.length);
while (true) {
//3.接收数据,将数据封装到数据包对象的字节数组中去
socket.receive(packet);//等待式接收数据。
//4.看看数据是否收到了
int len=packet.getLength(); //获取当前收到的数据长度。
String data=new String(buf,0,len);
System.out.println("服务端收到了:"+data);
//获取对方的IP对象和程序端口
String ip=packet.getAddress().getHostAddress(); //获取对方的IP
int port=packet.getPort(); //获取对方程序端口
System.out.println("对方IP:"+ip+" 对方端口:"+ port);
System.out.println("-------------------------------------------");
}
}
}
- 以上多收多方代码,服务端支持同时与多个客户端通信。
五)TCP通信
1.TCP通信的实现
(1)特点:面向连接、可靠通信。
(2)通信双方会实现次采取“三次握手”建立连接,实现端到端的通信;底层能保证数据成功传给服务端。
(3)Java提供了一个java.net.Socket类来实现TCP通信。
客户端开发
| 构造器 |
说明 |
| public Socket(String host,int port) |
根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端socket |
| 方法 |
说明 |
| public OutputStream getOutputStream() |
获得字节输出流对象 |
| public InputStream getInputStream() |
获得字节输入流对象 |
服务端开发(服务端是通过java.net包下的serverSocket类实现的)
| 构造器 |
说明 |
| public ServerSocket(int port) |
创建服务器的ServerSocket对象,为服务器程序注册端口 |
| 方法 |
说明 |
| public Socket accept() |
阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象 |
| public void close() |
关闭连接,释放资源 |
单发单收
public class ClientDemo1 {
//____________单发单收____________
public static void main(String[] args) throws Exception {
//实现TCP通信一发一收
System.out.println("客户端启动");
//1.常见Socket管道对象,请求与服务端的Socket连接。可靠连接
Socket socket = new Socket("192.168.137.1", 8888);
//2.从Socket通信管道中得到一个字节输出流,那么对面服务端也要是一个对应的字节输入流
OutputStream os = socket.getOutputStream();
//3.特殊数据流
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(1);
dos.writeUTF("你好,我是客户端");
//4.关闭资源
dos.close();
}
}
class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动");
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(8888);//注册端口号,建立连接
//2.调用accept方法,阻塞等待客户端连接,一有客户端连接会返回一个Socket对象
Socket socket = ss.accept();
//3.获取输入流,读取客户端发送的数据
InputStream is = socket.getInputStream();
//4.把字节输入流包装成特殊数据输入流
DataInputStream dis = new DataInputStream(is);
//5.读取数据
int id=dis.readInt();
String mag=dis.readUTF();
System.out.println("id="+id+",收到的客户端msg="+mag);
//6.获取客户端的端口与IP
System.out.println("客户端的IP:"+socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口="+socket.getPort());
}
}
多发多收
public class ClientDemo1 {
//____________多发多收_____________
public static void main(String[] args) throws Exception {
//实现TCP通信一发一收
System.out.println("客户端启动");
//1.常见Socket管道对象,请求与服务端的Socket连接。可靠连接
Socket socket = new Socket("192.168.137.1", 8888);
//2.从Socket通信管道中得到一个字节输出流,那么对面服务端也要是一个对应的字节输入流
OutputStream os = socket.getOutputStream();
//3.特殊数据流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc=new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String msg=sc.nextLine();
if ("exit".equals(msg)) {
System.out.println("退出");
socket.close();//关闭资源
break;
}
dos.writeUTF(msg);
dos.flush();//刷新
}
}
}
class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动");
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(8888);//注册端口号,建立连接
//2.调用accept方法,阻塞等待客户端连接,一有客户端连接会返回一个Socket对象
Socket socket = ss.accept();
//3.获取输入流,读取客户端发送的数据
InputStream is = socket.getInputStream();
//4.把字节输入流包装成特殊数据输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
//5.读取数据
String mag=dis.readUTF();
System.out.println("收到的客户端msg="+mag);
//6.获取客户端的端口与IP
System.out.println("客户端的IP:"+socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口="+socket.getPort());
System.out.println("------------------------------------------------");
}
}
}
-
目前开发的服务端程序,不支持同时与多个客户端通信,因为服务端现在只有一个主线程,只能处理一个客户端的消息。
-
支持服务端同时与多个客户端同时通信
(1)主线程定义了循环负责接收客户端Socket管道连接。
(2)每接收到一个Socket通信管道后分配一个独立的线程负责处理它。
点击查看代码
public class ClientDemo1 {
//____________多发多收_____________
public static void main(String[] args) throws Exception {
//实现TCP通信一发一收
System.out.println("客户端启动");
//1.常见Socket管道对象,请求与服务端的Socket连接。可靠连接
Socket socket = new Socket("192.168.137.1", 8888);
//2.从Socket通信管道中得到一个字节输出流,那么对面服务端也要是一个对应的字节输入流
OutputStream os = socket.getOutputStream();
//3.特殊数据流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc=new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String msg=sc.nextLine();
if ("exit".equals(msg)) {
System.out.println("退出");
socket.close();//关闭资源
break;
}
dos.writeUTF(msg);
dos.flush();//刷新
}
}
}
class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动");
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(8888);//注册端口号,建立连接
while (true) {
//2.调用accept方法,阻塞等待客户端连接,一有客户端连接会返回一个Socket对象
Socket socket = ss.accept();
System.out.println("客户端上线了:"+socket.getInetAddress().getHostAddress());
new ServerReader( socket).start();
}
}
}
class ServerReader extends Thread{
private Socket socket;
public ServerReader(Socket socket){
this.socket=socket;
}
@Override
public void run(){
try{
//读取管道的消息
//3.获取输入流,读取客户端发送的数据
InputStream is = socket.getInputStream();
//4.把字节输入流包装成特殊数据输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
//5.读取数据
String mag=dis.readUTF();
System.out.println("收到的客户端msg="+mag);
//6.获取客户端的端口与IP
System.out.println("客户端的IP:"+socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口="+socket.getPort());
System.out.println("------------------------------------------------");
}
}catch (Exception e){
System.out.println("客户端下线了:"+socket.getInetAddress().getHostAddress());
}
}
}
Ⅲ.B/S架构的原理
- 注意:服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据。
- HTTP协议规定:响应给浏览器的数据格式必须满足以下格式
![屏幕截图 2025-09-24 214048]()
点击查看代码
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动");
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(8080);//注册端口号,建立连接
while (true) {
//2.调用accept方法,阻塞等待客户端连接,一有客户端连接会返回一个Socket对象
Socket socket = ss.accept();
System.out.println("客户端上线了:"+socket.getInetAddress().getHostAddress());
//3.把这个客户端管道交给一个独立的子线程专门负责接收这个管道的消息。
new ServerReader( socket).start();
}
}
}
class ServerReader extends Thread{
private Socket socket;
public ServerReader(Socket socket){
this.socket=socket;
}
@Override
public void run(){
try{
//给当前对应的浏览器管道响应一个网页数据回去。
OutputStream os=socket.getOutputStream();
//通过字节输出流包装写出去的数据给浏览器
//把字节输出流包装成打印流。
PrintStream ps=new PrintStream(os);
//写响应的网页数据出去
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=utf-8");
ps.println();//必须换一行
ps.println("<html>");
ps.println("<head>");
ps.println("<meta charset='utf-8'/>");
ps.println("<title>");
ps.println("欢迎来到黑马程序员");
ps.println("</title>");
ps.println("</head>");
ps.println("<body>");
ps.println("<h1 style='color:red;font-size=20px'>欢迎来到黑马程序员</h1>");
//响应一个黑马程序员的log展示
ps.println("<img src='https://www.itheima.com/images/logo.png'>");
ps.println("</body>");
ps.println("</html>");
ps.close();
socket.close();
}catch (Exception e){
System.out.println("客户端下线了:"+socket.getInetAddress().getHostAddress());
}
}
}
- 每次请求都创建一个新线程,高并发,容易宕机!可以使用线程池优化,把所有Socket管道包装成任务队列。
点击查看代码
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动");
//1.创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(8080);//注册端口号,建立连接
//创建线程池
ExecutorService pool=new ThreadPoolExecutor(3,10,10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
while (true) {
//2.调用accept方法,阻塞等待客户端连接,一有客户端连接会返回一个Socket对象
Socket socket = ss.accept();
System.out.println("客户端上线了:"+socket.getInetAddress().getHostAddress());
//3.把这个客户端管道交给一个独立的子线程专门负责接收这个管道的消息。
pool.execute(new ServerReaderRunnable( socket));
}
}
}
class ServerReaderRunnable implements Runnable{ //extends Thread 线程对象也可以当Runnable任务对象用
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket=socket;
}
@Override
public void run(){
try{
//给当前对应的浏览器管道响应一个网页数据回去。
OutputStream os=socket.getOutputStream();
//通过字节输出流包装写出去的数据给浏览器
//把字节输出流包装成打印流。
PrintStream ps=new PrintStream(os);
//写响应的网页数据出去
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=utf-8");
ps.println();//必须换一行
ps.println("<html>");
ps.println("<head>");
ps.println("<meta charset='utf-8'/>");
ps.println("<title>");
ps.println("欢迎来到黑马程序员");
ps.println("</title>");
ps.println("</head>");
ps.println("<body>");
ps.println("<h1 style='color:red;font-size=20px'>欢迎来到黑马程序员</h1>");
//响应一个黑马程序员的log展示
ps.println("<img src='https://www.itheima.com/images/logo.png'>");
ps.println("</body>");
ps.println("</html>");
ps.close();
socket.close();
}catch (Exception e){
System.out.println("客户端下线了:"+socket.getInetAddress().getHostAddress());
}
}
}
(九)相关API
- 时间相关的获取方案
(1)LocalDate:代表本地日期(年、月、日、星期)
(2)LocalTime:代表本地时间(时、分、秒、纳秒)
(3)LocalDateTime:代表本地日期、时间(年、月、日、星期、时、分、秒、纳秒)
它们获取对象的方案
| 方法名 |
示例 |
| public static Xxxx now():获取系统当前时间对应的该对象 |
LocalDate ld=LocalDate.now(); |
|
LocalTime lt=LocalTime.now(); |
|
LocalDateTime ldt=LocalDateTime.now(); |
LocalDateTime的常用API (可以处理年、月、日、星期、时、分、秒、纳秒等信息)
| 方法名 |
说明 |
| getYear、getMonthValue、getDayOfMonth、getDayOfYear... |
获取年月日、时分秒、纳秒等 |
| withYear、withMonthValue、withDayOfMonth、withDayOfYear... |
修改某个信息,返回新日期时间对象 |
| plusYears、plusMonths、plusDays、plusWeeks... |
把某个信息加多少,返回新日期时间对象 |
| minusYears、minusMonths、minusDays、minusWeeks... |
把某个信息加多少,返回新日期时间对象 |
| equals、isBefore、isAfter |
判断2个时间对象,是否相等,在前还是在后 |
点击查看代码
public class Test1 {
public static void main(String[] args){
//java提供的获取时间的方案。
//JDK8之前的方案:Date 获取此刻日期时间 老项目还有。
Date d=new Date();
System.out.println(d);
//格式化:SimpleDateFormat 简单日期格式化,格式化日期对象成为我们喜欢的格式。
SimpleDateFormat sdf=new SimpleDateFormat("yyy年MM月dd日 HH:mm:ss EEE a");
String result=sdf.format(d);
System.out.println(result);
//JDK8之后:LocalDate LocalTime LocalDateTime 获取此刻日期时间 新项目推荐。
//获取此刻的日期时间对象
LocalDateTime now= LocalDateTime.now();
System.out.println(now);//2021-09-05T16:05:07.768
System.out.println(now.getYear());//获取年份
System.out.println(now.getDayOfYear());//获取年份的第几天
System.out.println(now.getMonthValue());//获取月份
System.out.println(now.getDayOfMonth());//获取月份的第几天
System.out.println(now.getDayOfWeek().getValue());//获取星期几
LocalDateTime now2=now.plusSeconds(60);//60秒后
System.out.println(now);
System.out.println(now2);
//格式化:DateTimeFormatter
//1.创建格式化对象
DateTimeFormatter dtf=DateTimeFormatter.ofPattern("yyy-MM-dd HH:mm:ss EE a");
//2.格式化now对象的时间
String result2=dtf.format(now);
System.out.println(result2);
}
}
- 字符串的高效操作
(1)对于字符串相关的操作,如频繁的拼接、修改等,建议使用StringBuilder,效率更高!
(2)注意:如果操作字符串较少,或者不需要操作,以及定义字符串变量,还是建议用String。
(3)StringBuilder代表可变字符串对象。相当于是一个容器,它里面装的字符串是可以改变的,就是用来操作字符串的。
(4)好处:StringBuilder比String更适合做字符串的修改操作,效率会更高,代码也会更简洁。
| 构造器 |
说明 |
| public StringBuilder() |
创建一个空白的可变的字符串,不包含任何内容 |
| public StringBuilder(String str) |
创建一个指定字符串内容的可变字符串对象 |
| 方法名称 |
说明 |
| public StringBuilder append(任意类型) |
添加数据并返回StringBuilder对象本身 |
| public StringBuilder reverse() |
将对象的内容反转 |
| public int length() |
返回对象内容长度 |
| public String toString() |
通过toString()就可以实现把StringBuilder转换为String |
点击查看代码
public static void main(String[] args){
//高效拼接字符串
//+ 号拼接字符串内容,如果大量拼接,效率极差!
//String的对象是不可变变量:共享数据性能可以,但修改数据性能差!
// String s="";
// for(int i=0;i<1000000;i++){
// s=s+"abc";
// }
// System.out.println(s);
//定义字符串可以使用String类型,但操作字符串建议使用StringBuilder(性能好)
StringBuilder sb=new StringBuilder();//StringBuilder对象是可变内容的容器 sb="";
for(int i=0;i<1000000;i++){
sb.append("abc");
}
System.out.println(sb);
//StringBuilder只是拼接字符串的手段,结果还是要恢复成字符串(目的)
String s=sb.toString();
System.out.println(s);
//StringBuilder对象可以进行链式操作
StringBuilder sb2=new StringBuilder();
String result=sb2.append("张三").append("李四").append("王五").toString();
System.out.println(result);
}
}
- BigDecimal
(1)用于解决浮点型运算时,出现结果失真的问题。
BigDecimal的常用构造器、常用方法
| 构造器 |
说明 |
| public BigDecimal(double val) 注意:不推荐使用这个 |
将double转换为BigDecimal |
| public BigDecimal(String val) |
把String转成BigDecimal |
| 方法名 |
说明 |
| public static BigDecimal valueOf(double val) |
转换一个double成BigDecimal |
| public BigDecimal add(BigDecimal b) |
加法 |
| public BigDecimal subtract(BigDecimal b) |
减法 |
| public BigDecimal multiply(BigDecimal b) |
乘法 |
| public BigDecimal divide(BigDecimal b) |
除法 |
| public BigDecimal divide(另一个BigDecimal对象,精确几位,舍入模式) |
除法、可以控制精确到小数几位 |
| public double doubleValue() |
将BigDecimal转换为double |
点击查看代码
public class Test3 {
public static void main(String[] args){
//掌握BigDecimal解决小数运算结果失真问题。
double a=0.1;
double b=0.2;
System.out.println(a+b);//0.30000000000000004
//使用BigDecimal
//1.把小数包装成BigDecimal对象来运算才可以。
//必须使用public BigDecimal(String val)字符串才能解决失真问题
// BigDecimal a1=new BigDecimal(Double.toString( a));
// BigDecimal b1=new BigDecimal(Double.toString( b));
//优化方案,可以直接调用valueOf方法,内部使用的就是public BigDecimal(String val)字符串构造器
BigDecimal a1=BigDecimal.valueOf(a);
BigDecimal b1=BigDecimal.valueOf(b);
BigDecimal c1=a1.add(b1); //解决精度问题的手段
double result=c1.doubleValue(); //目的:把BigDecimal对象转换成double类型
System.out.println(result);
System.out.println("----------------");
BigDecimal i=BigDecimal.valueOf(0.1);
BigDecimal j=BigDecimal.valueOf(0.3);
//除法
// BigDecimal c=i.divide(j);//抛出异常
BigDecimal k=i.divide(j,2, RoundingMode.HALF_UP);
System.out.println(k);
}
}
三、Java高级技术
(一)Junit单元测试框架
- 单元测试:针对最小功能单元:方法,编写测试代码对其进行正确性测试。
- Junit单元测试框架:可以用来对方法进行测试,它是第三方公司开源出来的(很多开发工具已经集成了Junit框架,比如IDEA)。
- 优点:
(1)可以灵活编写测试代码,可以针对某个方法执行测试,也支持一键完成对全部方法的自动化测试,且各自独立。
(2)不需要程序员去分析测试的结果,会自动生成测试报告出来。
- 具体步骤:
(1)将Junit框架的jar包导入到项目中(注意:IDEA集成了Junit框架,不需要我们自己手工导入了)
(2)为需要测试的业务类,定义对应的测试类,为每个业务方法,编写对应的测试方法(必须公共,无参,无返回值)
(3)测试方法上必须声明@Test注解,然后在测试方法中,编写代码调用被测试的业务方法测试。
(4)开始测试:选中测试方法,右键选择“Junit运行”,如果测试通过则为绿色,如果测试失败,则是红色。
(5)测试某个方法直接右键该方法启动测试,测试全部方法可以选择类或模块启动。
点击查看代码
/**
* 字符串工具类
*/
public class StringUtil {
public static void printNumber(String name){
if(name==null){
System.out.println("参数为null!请注意");
return;
}
System.out.println("名字长度是:"+name.length());
}
/**
* 求字符串的最大索引
*/
public static int getMaxIndex(String data){
if (data==null||"".equals( data)){
return -1;
}
return data.length()-1;
}
}
// 测试类:junit单元测试框架,对业务类中的方法进行正确性测试。
public class StringUtilTest {
// 测试方法:必须以公开public,无参,无返回值。
//测试方法必须加上@Test注解(Junit框架的核心步骤)
@Test
public void testPrinNumber(){
//测试步骤:
StringUtil.printNumber("张三abc");//5
//测试用例
StringUtil.printNumber("");
StringUtil.printNumber(null);
}
@Test
public void testGetMaxIndex(){
//测试步骤:
int index = StringUtil.getMaxIndex("abcdefg");//7
//测试用例
int index2 = StringUtil.getMaxIndex("");
int index3 = StringUtil.getMaxIndex(null);
System.out.println(index);
System.out.println(index2);
System.out.println(index3);
//做断言:断言结果是否与预期结果一致
Assert.assertEquals("本轮测试失败,业务获取的索引有问题!请检查",6,index);
Assert.assertEquals("本轮测试失败,业务获取的索引有问题!请检查",-1,index2);
Assert.assertEquals("本轮测试失败,业务获取的索引有问题!请检查",-1,index3);
}
}
(二)反射
Ⅰ.反射概述
- 反射:加载类,并允许以编程的方法解剖类中的各种成分(成员变量、方法、构造器等)。
- 反射具体是学获取类的信息、操作它们:
(1)反射第一步:加载类,获取类的字节码:Class对象
(2)获取类的构造器:Constructor对象
(3)获取类的成员变量:Field对象
(4)获取类的成员方法:Method对象。
- 反射第一步:加载类,获取类的Class对象——获取Class对象的三种方式:
(1)Class c1=类名.class
(2)调用Class提供的方法:public static Class forName(String package);
(3)Object提供的方法:public Class getClass(); ( Class c3=对象.getClass() )
点击查看代码
public class ReflectDemo1 {
public static void main(String[] args) throws Exception {
//掌握反射第一步操作:获取Class对象。(获取类本身)
//1.获取类本身:类.class
Class c1 = Student.class;
System.out.println(c1);
//2.获取类本身:Class.forName("类的全类名")
Class c2 = Class.forName("com.itheima.demo2reflect.Student");
System.out.println(c2);
//3.获取类本身:对象.getClass()
Student s = new Student();
Class c3 = s.getClass();
System.out.println(c3);
System.out.println(c1==c2);// true
System.out.println(c2==c3);// true
}
}
Ⅱ.反射获取类中的成分、并对其进行操作
- 获取类的构造器
Class提供了从类中获取构造器的方法
| 方法 |
说明 |
| Constructor<?>[] getConstructors( ) |
获取全部构造器(只能获取public修饰的) |
| Constructor<?>[] getDeclaredConstructors( ) |
获取全部构造器(只要存在就能拿到) |
| Constructor getConstructor( Class<?>...parameterTypes) |
获取某个构造器(只能获取public修饰的) |
| Constructor getDeclaredConstructor( Class<?>...parameterTypes |
获取某个构造器(只要存在就能拿到) |
获取构造器的作用:依然是初始化对象返回
| Constructor提供的方法 |
说明 |
| T newInstance(Object...initargs) |
调用此构造器对象表示的构造器,并传入参数,完成对象的初始化并返回 |
| public void setAccessible(boolean flag) |
设置为true,表示禁止检查访问控制(暴力反射) |
- 获取类的成员变量
Class提供了从类中获取成员变量的方法。
| 方法 |
说明 |
| public Field[] getFields() |
获取类的全部成员变量(只能获取public修饰的) |
| public Field[] getDeclaredFields() |
获取类的全部成员变量(只要存在就能拿到) |
| public Field getField(String name) |
获取类的某个成员变量(只能获取public修饰的) |
| public Field getDeclaredField(String name) |
获取类的某个成员变量(只要存在就能拿到) |
获取到成员变量的作用:依然是赋值、取值。
| 方法 |
说明 |
| void set(Object obj,Object value): |
赋值 |
| Object get(Object obj) |
取值 |
| public void setAccessible(boolean flag) |
设置为true,表示禁止检查访问控制(暴力反射) |
- 获取类的成员方法
Class提供了从类中获取成员方法的API。
| 方法 |
说明 |
| Method[] getMethods() |
获取类的全部成员方法(只能获取public修饰的) |
| Method[] getDeclaredMethods() |
获取类的全部成员方法(只要存在就能拿到) |
| Method[] getMethod(String name,class<?>...parameterTypes) |
获取类的某个成员方法(只能获取public修饰的) |
| Method[] getDeclaredMethod(String name,class<?>...parameterTypes) |
获取类的某个成员方法(只要存在就能拿到) |
成员方法的作用:依然是执行
| Method提供的方法 |
说明 |
| public Object invoke(Object obj,Object...args) |
触发某个对象的该方法执行。 |
| public void setAccessible(boolean flag) |
设置为true,表示禁止检查访问控制(暴力反射) |
点击查看代码
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectDemo2 {
@Test
public void getClassInfo(){
//获取类的消息
//1.反射的第一步:获取Class对象,代表拿到类。
Class c1 = Student.class;
System.out.println(c1.getName());//类名的全类名 com.itheima.demo2reflect.Student
System.out.println(c1.getSimpleName());//类名 Student
}
//2.获取类的构造器对象并对其进行操作。
@Test
public void getConstructorInfo() throws Exception {
//获取类的构造器对象并对其进行操作。
//1.反射的第一步:获取Class对象,代表拿到类。
Class c1 = Dog.class;
//2.获取构造器对象。
Constructor[] cons=c1.getDeclaredConstructors();
for (Constructor con:cons){
System.out.println(con.getName()+"("+con.getParameterCount()+")");
}
//3.获取单个构造器
Constructor con=c1.getDeclaredConstructor();//获取无参构造器
System.out.println(con.getName()+"("+con.getParameterCount()+")");
Constructor con2=c1.getDeclaredConstructor(int.class,String.class);//2个参数的有参数构造器
System.out.println(con2.getName()+"("+con2.getParameterCount()+")");
//4.获取构造器的作用依然是创建对象:创建对象。
//暴力反射:暴力反射可以访问私有的构造器、方法、属性。
con.setAccessible(true);//绕过访问权限,直接访问!
Dog d1=(Dog)con.newInstance();
System.out.println(d1);
Dog d2=(Dog)con2.newInstance(10,"小花");
System.out.println(d2);
}
//3.获取类的成员变量对象并对其进行操作
@Test
public void getFieldInfo() throws Exception {
//获取类的成员变量对象并对其进行操作
//1.反射的第一步:获取Class对象,代表拿到类。
Class c1 = Dog.class;
//2.获取成员变量对象
Field[] fields=c1.getDeclaredFields();
for (Field field:fields){
System.out.println(field.getName()+"("+field.getType()+")");
}
//3.获取单个成员变量对象
Field field=c1.getDeclaredField("hobby");
System.out.println(field.getName()+"("+field.getType().getName()+")");
Field field2=c1.getDeclaredField("age");
System.out.println(field2.getName()+"("+field2.getType().getName()+")");
//4.获取成员变量的目的依然是取值和赋值
Dog d=new Dog(3,"泰迪");
field.setAccessible(true);//绕过访问权限,直接访问!
field.set(d, "社交");// d.setHobby("社交");
System.out.println(d);
String hobby= (String) field.get(d);// d.getHobby();
System.out.println(hobby);
}
//4.获取类的成员方法对象并对其进行操作
@Test
public void getMethodInfo() throws Exception {
//获取类的成员方法对象并对其进行操作
//1.反射的第一步:获取Class对象,代表拿到类。
Class c1 = Dog.class;
//2.获取成员方法对象
Method[] methods=c1.getDeclaredMethods();
for (Method method:methods){
System.out.println(method.getName()+"("+method.getParameterCount()+")");
}
//3.获取单个成员方法对象
Method m1=c1.getDeclaredMethod("eat");//获取的是无参数的eat方法
Method m2=c1.getDeclaredMethod("eat",String.class);//获取的是有参数的eat方法
System.out.println(m1.getName()+"("+m1.getParameterCount()+")");
System.out.println(m2.getName()+"("+m2.getParameterCount()+")");
//4.获取成员方法的目的依然是调用方法。
Dog d=new Dog(3,"泰迪");
m1.setAccessible(true);//绕过访问权限,直接访问!
Object rs1=m1.invoke(d);//唤醒对象d的eat方法执行,相当于d.eat();
System.out.println(rs1);//null
Object rs2=m2.invoke(d,"牛肉");//唤醒对象d的eat带String参数的方法执行,相当于d.eat("牛肉");
System.out.println(rs2);
}
}
class Dog {
private String name;
private int age;
private String hobby;
private Dog(){
System.out.println("无参构造器执行了!");
}
public Dog(String name) {
System.out.println("1个参数有参构造器执行了!");
this.name = name;
}
public Dog(int age, String name) {
System.out.println("2个参数有参构造器执行了!");
this.age = age;
this.name = name;
}
private void eat(){
System.out.println("狗吃骨头!");
}
public String eat(String name){
System.out.println("狗吃"+name);
return "狗说:谢谢!汪汪汪!";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
", hobby='" + hobby + '\'' +
'}';
}
}
Ⅲ.反射的作用、应用场景
- 反射作用:
(1)基本作用:可以得到一个类的全部成分然后操作。
(2)可以破坏封装性。
(3)可以绕过泛型的约束。
(4)最重要的用途是:适合做Java的框架,基本上,主流的框架都会基于反射设计出一些通用的功能(如SpringBoot)。
点击查看代码
public class ReflectDemo4 {
public static void main(String[] args) throws Exception {
//搞清楚反射的应用:做框架的通用技术。
Dog d=new Dog(3,"小黑");
SaveObjectFrameWork.saveObject(d);
//创建学生对象
Student s=new Student("小明",18,"java");
SaveObjectFrameWork.saveObject(s);
//创建老师对象
Teacher t=new Teacher("小王",18,"python",5000,"软件工程",'男',"12345678901");
SaveObjectFrameWork.saveObject(t);
}
}
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
public class SaveObjectFrameWork {
//保存任意对象的静态方法
public static void saveObject(Object obj) throws Exception{
PrintStream ps=new PrintStream(new FileOutputStream("day06-junit-reflect-annotation-proxy\\src\\obj.txt",true));
//obj 可能是学生 老师 狗
//只有反射可以知道对象有多少个字段
//1.获取Class对象
Class c=obj.getClass();
String simpleName=c.getSimpleName();
ps.println("==============="+simpleName+"===============");
//2.获取Class对象的索引字段
Field[] fields=c.getDeclaredFields();
//3.遍历字段
for (Field field:fields){
//4.获取字段的值
//获取字段名称
String fieldName=field.getName();
field.setAccessible(true);
//获取字段的值
Object fieldValue=field.get(obj)+"";
//5.打印到文件中去
ps.println(fieldName+"="+fieldValue);
}
ps.close();
}
}
(三)注解
- 注解概述
(1)就是java代码里的特殊标记,如:@Override,@Test等,作用:让其他程序根据注解信息来决定怎么执行该程序。
(2)注意:注解可以用在类上、构造器上、方法上、成员变量上、参数上、等位置处。
Ⅰ.自定义注解
- 自定义注解
public @interface 注解名称{
public 属性类型 属性名() default 默认值;
}
- 特殊属性名:value
如果注解中只有一个value属性,使用注解时,value名称可以不写!!
- 注解的原理
(1)注解本质是一个接口,Java中所有注解都是继承了Annotation接口。
(2)@注解(...):其实就是一个实现类对象,实现了该注解以及Annotation接口。
点击查看代码
@MyBook(name = "java", age = 18, address = {"北京", "上海"})
//@A(value="delete")
//@A("delete")//特殊属性value,在使用时如果只有一个value属性,value名称可以不写
//@A(value = "delete",hobby = "看电影")
@A("delete")//如果hobby有默认值,可以不写hobby value
public class AnnotationDemo1 {
}
@interface A {
String value();//特殊属性value,在使用时如果只有一个value属性,value名称可以不写
String hobby() default "看电影";
}
@interface MyBook {
String name();
int age() default 18;
String[] address();
}
- 元注解
指的是:注解注解的注解。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test{
}
![屏幕截图 2025-10-01 170154]()
![屏幕截图 2025-10-01 170206]()
Ⅱ.注解的解析
- 就是判断类上、方法上、成员变量上是否存在注解,并把注解里的内容给解析出来。
- 如何解析注解?
(1)指导思想:要解析谁上面的注解,就应该先拿到谁。
(2)比如要解析类(成员方法)上面的注解,则应该先获得该类(成员方法)的Class(Method)对象,再通过Class(Method)对象解析其上面的注解。
(3)Class、Method、Fiel、Constructor,都实现了AnnotatedElement接口,它们都拥有解析注解的能力。
| AnnotatedElement接口提供了解析注解的方法 |
说明 |
| public Annotation[] getDeclaredAnnotations() |
获取当前对象上面的注解。 |
| public T getDclaredAnnotation(Class< T > annotationClass) |
获取指定的注解对象。 |
| public boolean isAnnotationPresent(Class annotationClass) |
判断当前对象上是否存在某个注解 |
点击查看代码
public class AnnotationDemo3 {
//解析注解
@Test
public void parseClass() throws Exception{
//1.获取类对象
Class c1=Demo.class;
//2.使用isAnnotationPresent判断这个类上是否陈列了注解MyTest2
if(c1.isAnnotationPresent(MyTest2.class)){
//3.获取注解对象
MyTest2 myTest2=(MyTest2) c1.getDeclaredAnnotation(MyTest2.class);
//4.获取注解的属性值
String[] address=myTest2.address();
double height=myTest2.height();
String value=myTest2.value();
//5.打印注解的属性值
System.out.println(Arrays.toString(address));
System.out.println(height);
System.out.println(value);
}
}
@Test
public void parseMethod() throws Exception{
//1.获取类对象
Class c1=Demo.class;
//2.获取方法对象
Method method=c1.getDeclaredMethod("go");
//3.用着isAnnotationPresent判断这个方法上是否陈列了注解MyTest2
if(method.isAnnotationPresent(MyTest2.class)){
//4.获取注解对象
MyTest2 myTest2=method.getDeclaredAnnotation(MyTest2.class);
//5.获取注解的属性值
String[] address=myTest2.address();
double height=myTest2.height();
String value=myTest2.value();
//6.打印注解的属性值
System.out.println(Arrays.toString(address));
System.out.println(height);
System.out.println(value);
}
}
}
@Target({ElementType.METHOD,ElementType.TYPE})//表示注解的作用目标为方法,成员变量
@Retention(RetentionPolicy.RUNTIME)//表示注解的保留策略:编译器运行时(一直活着)
public @interface MyTest2 {
String value();
double height() default 169.5;
String[] address();
}
@MyTest2(value = "李四",address = {"北京","上海","广州"})
public class Demo {
@MyTest2(value = "张三",address = {"北京","上海"})
public void go(){
}
}
Ⅲ.注解的应用场景
点击查看代码
public class AnnotationDemo4 {
//搞清楚注解的应用场景:模拟junit框架。有MyTest注解的方法就执行,没有就不执行
public static void main(String[] args) throws Exception{
AnnotationDemo4 ad=new AnnotationDemo4();
//1.获取类对象
Class c=AnnotationDemo4.class;
//2.获取所有方法对象
Method[] methods=c.getMethods();
//3.遍历所有方法,判断方法上是否有MyTest注解,有就执行,没有就不执行
for (Method method:methods){
//4.判断方法上是否有MyTest注解
if (method.isAnnotationPresent(MyTest.class)){
//获取到这个方法的注解
MyTest myTest=method.getDeclaredAnnotation(MyTest.class);
int count=myTest.count();
//5.有就执行这个method方法
for (int i=0;i<count;i++) {
method.invoke(ad);
}
}
}
}
//测试方法:public 无参 无返回值
@MyTest
public void test1(){
System.out.println("test1方法执行了...");
}
public void test2(){
System.out.println("test2方法执行了...");
}
public void test3(){
System.out.println("test3方法执行了...");
}
@MyTest(count=5)
public void test4(){
System.out.println("test4方法执行了...");
}
}
@Target({ElementType.METHOD})//表示注解的作用目标为方法
@Retention(RetentionPolicy.RUNTIME)//表示注解的保留策略:编译器运行时(一直活着)
public @interface MyTest {
int count() default 1;
}
(四)动态代理
- 动态代理:在运行时,根据接口创建代理对象,代理对象调用接口方法,实际调用的是代理对象的方法。
- 为Java对象创建一个代理对象
java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法:
oublic static Objact newProxyInstance(ClassLoader loader,Class<?>[] interface,InvocationHandler h)
参数一:用于指定用哪个类加载器,去加载生成的代理对象
参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
参数三:用来指定生成的代理对象要干什么事情
点击查看代码
package com.itheima.demo4proxy;
//明星行为接口
public interface StarService {
void sing(String name);
String dance();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Star implements StarService{
private String name;
@Override
public void sing(String name) {
System.out.println(this.name+"表演唱歌:"+name);
}
@Override
public String dance() {
System.out.println(this.name+"表演跳舞:魅力四射!");
return "感谢观看!";
}
}
/**
* 代理工具类:中介公司,专门负责创建代理对象并返回给别人使用
*/
public class ProxyUtil {
public static StarService createProxy(Star s){
/**
* 参数一:用于指定用哪个类加载器,去加载生成的代理类
* 参数二:用于指定代理类需要实现的接口:明星类实现了那些接口,代理类就实现哪些接口
* 参数三:用于指定代理类需要如何去代理(代理要做的事情)
*/
StarService proxy=(StarService) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
s.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//用来声明代理对象要干的事情。
//参数一:proxy接收到代理对象本身(暂时用处不大)
//参数二:method代表正在被代理的方法
//参数三:args代表正在被代理的方法的参数
String methodName=method.getName();
if ("sing".equals(methodName)){
System.out.println("准备话筒,收费20万!");
}else if ("dance".equals(methodName)){
System.out.println("准备场地,收费100万!");
}
//真正干活(把真正的明星对象叫过来正式干活)
//找真正的明星对象来执行被代理的行为:method方法
Object result=method.invoke(s,args);
return result;
}
});
return proxy;
}
}
public class Test {
public static void main(String[] args) {
//创建代理对象。
//1.准备一个明星对象:设计明星类。
Star star=new Star("张若楠");
//2.为张若楠创建一个专属于她的代理对象。
StarService proxy=ProxyUtil.createProxy(star);
proxy.sing("《红昭愿》");
System.out.println(proxy.dance());
}
}