文章目录
- day01之静态&继承
- day02之多态&抽象类&接口&常量&枚举
- day03之内部类&泛型&常用API
- day04之常见API&日期时间&Arrays
- day05之==异常==&==Lambda==&&方法引用&常见算法&正则表达式
- day06之集合->Collection&List&Set
- day07之集合->Map&Stream&递归
- day08之IO流->文件&字节流
day01之静态&继承
内容介绍
封装就是把属性私有化,然后对外提供set/get方法。
所谓面向对象,其实是一种写程序的套路。
面向过程:需要一个功能,就要写一个方法。
面向对象:有一个功能要做,要创建一个对象,在对象里提供对应的属性和方法,属性用来保存数据,方法用来操作(处理)数据。只要需要这个功能,调用对象的对应方法即可。
静态:成员变量name,score是属于每个对象的,每个对象中都有一份该数据。有些数据要被所有对象共享,应该用static修饰。
继承:系统中定义了很多实体类,其中有很多属性、行为存在重复代码,要对代码进行优化以便降低代码冗余,提升代码复用,需要继承。
一、static:静态
1.static修饰成员变量
1.1概念
● static叫静态,可以修饰成员变量、成员方法。
1.2成员变量按照有无static修饰分为类变量(静态成员变量)和实例变量
成员变量按照有无static修饰分为两种:
● 类变量:有static修饰,属于类,在计算机中只有一份,被类的全部对象共享。
类变量属于类,与类一起加载一次,在内存中只有一份,可以被类和类的所有对象共享。
● 实例变量(对象的变量):无static修饰,属于每个对象自己。
实例变量属于对象,每个对象中都有一份,只能用当前对象自己访问。
1.3类变量的两种访问形式
类变量有两种访问形式:
类名.类变量(推荐使用)
对象.类变量(不推荐)
1.4创建一个新工程的步骤
基础班的工程不再使用,创建一个就业班新工程(project):
File——>close project——>new project——>empty project——>填写project location——>填写project name——>finish——>create——>project structure——>projects——>project SDK:JDK17;project language level:16——>apply——>OK——>file——>new——>module——>Java——>next——>填写module名——>finish——>导包至src下
package com.itheima.a_static_修饰成员变量;
/*
static关键字
静态的意思,可以修饰成员变量、成员方法
成员变量按照有无static修饰,分为两种
类变量:有static修饰,属于类,随着类的加载而加载,在计算机中只有一份,会被类的所有对象共享
方式1: 类名.类变量(推荐)
方式2: 对象名.类变量(不推荐)
实例变量:无static修饰,属于每个对象自己
对象名.实例变量
总结
类变量既可以使用类名调用,也可以使用对象名调用,推荐类名方式
实例变量只能使用对象名调用
*/
public class Demo {
public static void main(String[] args) {
//需求1: 访问Student中的类变量name,赋值张三
//形式1:类名.变量名
Student.name = "张三";
//形式2:对象名.变量名
Student student = new Student();
student.name = "李四";//不给代码提示,idea不推荐但可以用
//需求2: 访问Student中的实例变量age,赋值18
student.age = 18;
//Student.age
//需求3: 获取Student的name和age的值,打印在控制台
System.out.println(Student.name);
System.out.println(student.age);
}
}
class Student {
//定义1个类变量: 记录学生姓名name
//有static修饰,属于类,随着类的加载而加载,在计算机中只有一份,会被类的所有对象共享
static String name;
//定义1个实例变量, 记录学生的年龄age
//无static修饰,属于每个对象自己
int age;
}
1.5总结:
1.6成员变量在内存中的执行原理
字节码文件(.class文件)进入方法区
方法进入栈内存
对象(new出来的东西)进入堆内存
流转过程:
1.首先Test类加载到方法区,然后读取main方法,读取后main方法入栈。
2.入栈以后读里面的内容,Student.name="袁华"
,访问Student类,此时Student类进入方法区
3.然后开始给这个类进行赋值,给类里的name进行赋值,name是一个静态成员变量,在给name赋值的过程中,没有 new 对象,但是在堆内存中也开辟了空间,存储的是静态变量,然后给name赋值为"袁华"
4.接下来开始创建对象,每次new对象,栈内存和堆内存都会出现东西,栈内存出现s1,堆内存出现student。
栈中s1指向堆内存中的student对象,在student对象中只有一个实例变量age属性。
若要访问student的静态变量name属性,顺序是先找到对象,再找到类,再找到堆内存中存储的静态变量
5.然后给Student对象s1的name赋值为马冬梅,会把堆内存中静态变量name修改为马冬梅。
6.接着创建Student对象s2,并给其name赋值为秋雅。马冬梅被替换为秋雅。
7.如上,s1和s2修改静态成员变量name时,修改的是堆内存中的同一个静态变量,也就是所谓的,类变量(静态成员变量)是这个类的所有对象共享的一块区域。
8.最后,不管通过s1还是s2,还是Student类,只要打印name,值都是“秋雅”。
对于实例变量,同时修改Student对象s1和Student对象s2的age,堆内存中这两个对象的age值发生变化,和Student类没有关系。
接下来打印s1的age值就是23
若用Student类的类名访问age,访问不到,因为不能用一个类访问实例变量。
类变量有的时候,实例变量不一定有,所以无法访问。只有后边的可以访问前边的,前边的不能访问后边的。
2.static修饰成员变量的应用场景
2.1类变量(静态成员变量)的应用场景
在开发中,若某个数据只需要一份,且希望能被共享(访问、修改),该数据可被定义成类变量来记住。
比如下图中的场景:
2.2总结
3.static修饰成员方法
3.1成员方法按照有无static修饰分为类方法和实例方法
==类方法:==有static修饰的成员方法,属于类。
public static void printHelloWorld(){
System.out.println("Hello World!");
System.out.println("Hello World!");
}
==实例方法:==无static修饰的成员方法,属于对象。
public void printPass(){
...
}
3.2类方法和实例方法的调用方式
类方法的调用方式:
1.类名.类方法(推荐)
2.对象名.类方法
实例方法的调用方式:
对象.实例方法
3.3总结
3又四分之一.搞懂main方法
public class Test{
public static void main(String[] args){
...
}
}
main方法是JVM调用程序的入口,main方法也被static修饰,它也属于类方法(静态成员方法)。
在写main方法时,必须用public static void
修饰,只要知道类名,JVM知道类名,JVM在底层直接用类名.main,就相当于在调用main方法。
底层调用main方法的时候,就是JVM在java 类名
(这里的java是执行),通过java命令把类名传过去,就是类名在调用main方法。
需要研究的是,main方法里传的参数String[ ] args
是什么。
args是main方法的形参,JVM调用main方法的时候,是可以给main方法传参数的。
一般定义main方法的时候都不会传参数,但比如希望程序延迟运行,而这个延迟时间在启动的时候通过args参数传递。
/*
main方法
Java程序的入口方法, 被static修饰, 是一个静态方法
JVM在运行类的时候, 会自动调用: 类名.main(args);
args
main方法的形参, jvm在调用main方法的时候是可以给main方法传参数的
*/
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
//args用于接收jvm传过来的参数
//1.打印数组
System.out.println(Arrays.toString(args));
//比如:我们希望程序延迟运行, 而延迟时间是启动的时候通过参数传递
//获取参数数组中的第一个元素 5000
String arg = args[0];
int time = Integer.parseInt(arg);//把一个字符串转成int类型
//等待5秒之后,再继续运行
Thread.sleep(time);
//执行程序
System.out.println("程序开始执行...");
}
}
3又四分之一.2给main方法传参的方式
给main方法传参的方式:
可在上图中Program Arguments
处传参数,比如传一个数组:5000 1000 1000
4.static修饰成员方法的应用场景
4.1类方法的常见应用场景
类方法最常见的应用场景是做工具类
4.2工具类是什么
1.一些实用的、具有独立功能的方法,希望这些方法可被重复使用(复用),就像一个个小工具一样。
2.这些方法所在的类,就称为工具类。
3.工具类中的方法,习惯上设置为类方法(静态方法)。
4.工具类没有创建对象的需求,所以建议将工具类的构造器进行私有。
4.3用类方法来设计工具类有什么好处
相比于实例方法,类方法可以直接用类名调用,比较方便,而且不用创建对象,节省内存。
4.4总结
5.static的注意事项
1.类方法(静态成员方法)中可以直接访问类的成员,不能直接访问实例成员。
2.实例方法既可以直接访问类成员(静态成员),也可以直接访问实例成员。
3.实例方法中可以出现this关键字,类方法(静态成员方法)中不可以出现this关键字。
6.static的应用知识:代码块
6.1代码块概述
代码块是类的五大成分之一:
成员变量、构造器、方法、代码块、内部类
6.2代码块按照有无static修饰分为静态代码块和实例代码块
静态代码块:
1.格式:static { }
2.特点:类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次。
3.作用:完成类的初始化,比如:对类变量的初始化赋值。
实例代码块:
1.格式:{ }
2.特点:每次创建对象时,执行实例代码块,并在构造器(构造函数 / 构造方法)之前执行。
3.作用:和构造器一样,都是用来完成对象的初始化,比如:对实例变量进行初始化赋值。
6.3总结
7.总结
二、继承
Java中提供了关键字extends,使用此关键字,可以让一个类和另一个类建立父子关系。
public class B extends A{
//B类称为子类(派生类)
}
1.Java中继承的特点
子类能继承并使用父类中的非私有成员(成员变量、成员方法)
2.继承后对象的创建
子类的对象由子类、父类共同完成。
day02之多态&抽象类&接口&常量&枚举
多态两大体现、应用:抽象类、接口
一、面向对象特征之:多态
1.多态的概念
继承:父类和子类间即为继承关系
实现:实现接口(interface)
1.1什么是多态
多态是在继承(extends)或者实现(implements)情况下的一种现象,表现为:对象多态、行为多态。
对于对象多态的帮助理解:
作为人这个对象,角色不是固定的,可以是学生对象、路人对象、子女对象、老公对象、父母对象,人这个对象呈现出来的是多种形态,就称为对象多态。
对于行为多态的帮助理解:
对于同一个动作,写代码,有的同学敲得快,有的同学敲得慢。即对于同一个行为,每个对象表现出的形式也不同,此称为行为多态。
1.2多态的具体代码体现:
//前面范围大 后面范围小
People p1 = new Student();
p1.run();
People p2 = new Teacher();
p2.run();
1.3多态的前提:
1.有继承/实现关系;
2.存在父类引用指向子类对象;
3.存在方法重写。
1.4多态的注意事项
多态是对象、行为的多态,Java中的属性(成员变量)不谈多态。
变量没有多态一说,用的一直是左边的。
package com.itheima.a_多态_认识多态;
/*
多态
是在继承/实现情况下的一种现象, 表现为对象多态和行为多态
多态的前提
1. 有继承/实现关系
2. 有方法的重写
3. 存在父类引用指向子类对象
多态的注意事项
多态是对象、行为的多态; 变量则不涉及多态 没有变量多态
*/
public class Demo {
public static void main(String[] args) {
//使用多态的形式创建Student对象
//父类 变量 = new 子类();
People p1 = new Student();
p1.run();//编译看左边 运行看右边
//使用多态的形式创建Teacher对象
People p2 = new Teacher();
p2.run();
}
}
//定义父类People, 内含跑步run方法
class People{
String name = "人";
public void run(){
System.out.println("人在跑");
}
}
//定义子类Student, 内含跑步run方法
class Student extends People{
String name = "学生";
@Override
public void run() {
System.out.println("学生跑得快");
}
}
//定义子类Teacher, 内含跑步run方法
class Teacher extends People{
String name = "老师";
@Override
public void run() {
System.out.println("老师跑得慢");
}
}
1.5总结
2.多态的好处和弊端
以上就是原来面向对象的写法。
所谓多态只是把左边换成了父类。
2.1多态的好处
多态的好处:
1.多态形式下,等号左右两边松耦合,更便于修改和维护。
2.定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
1.多态形式下,等号左右两边松耦合,更便于修改和维护。
耦合即紧密联系,松耦合就是降低紧密联系。
//好处1: 在多态形式下,等号左右两边松耦合,更便于修改和维护
//创建一个老师对象,调用5次跑步的方法=======有一天需求改了========>创建一个学生对象,调用5次跑步的方法
//Person person = new Teacher();
Person person = new Student();
person.run();
person.run();
person.run();
person.run();
person.run();
person.run();
2.定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
public static void main(String[] args) {
run(new Teacher());
}
//好处2: 在多态下, 定义方法时, 可以使用父类类型作为形参, 那么该方法可以接收该父类下所有子类的对象
//创建一个run方法,接收一个老师对象,然后调用对象的run方法=======有一天需求改了========>接收一个学生对象,然后调用对象的run方法
public static void run(Person person) {
person.run();
}
2.2多态的弊端
多态下不能使用子类的独有功能。
解决方案:强制类型转换
p instanceof Student:判断p是不是Student类型
/*
多态的弊端
不能直接使用子类特有的功能
解决方案: 强制类型转换
多态中的转型
子-->父 (小到大 自动转换): 也称为向上转型, 父类引用指向子类对象 Person p = new Student();
父-->子 (大到小 强制转换): 也称为向下转型, 父类引用转为子类对象 Student s = (Student)p;
强转风险
强转是存在风险的, 如果转为父类引用记录的真实子类对象,那么不会报错(否则会报ClassCastException)
如果想规避这个风险,可以在强转前,使用instanceof关键字, 判断变量对应的类型
*/
public static void main(String[] args) {
//传入一个老师对象
run(new Teacher());
run(new Student());
}
public static void run(Person person) {
//调用老师的run()方法
person.run();
//需求: 想再调用一下老师的teach()方法
//person.teach();
//先判断person保存是不是一个Teacher的对象
if (person instanceof Teacher) {
Teacher teacher = (Teacher) person;
teacher.teach();
}
//扩展需求:老师——>teach 学生——>study
// 判断如果是学生,调用study
if (person instanceof Student) {
Student student = (Student) person;
student.study();
}
}
2.3总结
二、final
多态的重点应用在抽象类和接口。
final是前置知识。
1.final的使用
final关键字是最终的意思,可以修饰类、方法、变量。
1.被final修饰的类:最终类,不能再被继承
2.被final修饰的方法:最终方法,不能被重写
3.被final修饰的变量:只能被赋值一次,赋值完毕后不能再修改
3.1被final修饰的成员变量:声明时或者在构造方法结束之前完成赋值。
3.2被final修饰的局部变量:在使用之前完成赋值。
2.final修饰变量的注意事项:
● final修饰基本类型的变量,变量存储的数据不能被改变。
● final修饰引用类型的变量,变量存储的地址不能被改变,但地址所指向对象的内容可以被改变。
此时s一定要指向堆内存中的0x15ae7ab这块地址空间,但是空间中的内容可以改变
/*
final关键字是最终的意思,可以修饰(类、方法、变量)
修饰类:该类被称为最终类, 类不能再被继承
修饰方法:该方法被称为最终方法, 方法不能被重写
修饰变量:该变量只能被赋值一次, 赋值完毕之后不能再修改
成员变量: 声明时赋完值,或者在构造方法结束之前完成赋值
局部变量: 变量只能被赋值一次
final修饰变量的注意事项
基本类型变量: 变量记录的数据不能再被改变
引用类型变量: 变量记录的地址不能再被改变, 但是地址对应的堆内存中的内容可以改变
*/
public class Demo {
public static void main(String[] args) {
final Son son = new Son("大头");
//son = new Son("小头");//Cannot assign a value to final variable 'son'
//son.name="xxx";//编译不通过是因为name也被final修饰了
son.school="黑马";
son.school="白马";
System.out.println(son.school);
}
}
//final修饰类:该类被称为最终类,类不能再被继承
//Cannot inherit from final 'com.itheima.d_final.Father'
class Father {
//final修饰方法:该类被称为最终方法,方法不能被重写
//public final void run——>overridden method is final
//'run()' cannot override 'run()' in 'com.itheima.d_final.Father'; overridden method is final
public void run() {
System.out.println("爸爸在跑");
}
//'run()' cannot override 'run()' in 'com.itheima.d_final.Father'; overridden method is final
}
class Son extends Father {
final String name;
String school;
@Override
public void run() {
final int age = 10;
System.out.println(age + "岁的儿子在跑");
}
public Son(String name) {
this.name = name;
}
public Son(String name, String school) {
this.name = name;
this.school = school;
}
}
3.总结
三、抽象类
1.抽象类的概念
1.1什么是抽象类
● Java中有个关键字abstract,它就是抽象的意思,可以用它修饰类、成员方法。
● abstract修饰类,这个类就是抽象类;
abstract修饰方法,这个方法就是抽象方法。
修饰符 abstract class 类名{
修饰符 abstract 返回值类型 方法名称(形参列表);
}
public abstract class A{
//抽象方法:必须用abstract修饰,只有方法签名,不能有方法体。
//只有方法签名,没有方法的实现,等着子类来实现
public abstract void test();
}
1.2抽象类的特点
1.抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
2.类该有的成员(成员变量、方法、构造器),抽象类都可以有。
3.抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
4.一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须用abstract修饰(即定义成抽象类)。
/*
抽象类和抽象方法
在Java中有一个关键字叫:abstract,它就是抽象的意思,可以用它修饰类、成员方法。
abstract修饰类,这个类就是抽象类
abstract修饰方法,这个方法就是抽象方法,抽象方法没有方法体
抽象类的特点
1. 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
2. 类该有的成员(成员变量、方法、构造器)抽象类都可以有。
3. 抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
4. 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
*/
public class Demo {
public static void main(String[] args) {
//Person person = new Person("张三");//'Person' is abstract; cannot be instantiated
//多态
//父类引用指向子类对象
Person person = new Student();
person.run();
//3. 抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
//Person person1 = new Person();
//'Person' is abstract; cannot be instantiated抽象类不能被实例化
}
}
//1. 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
//需求1: 将Person类声明为抽象类
abstract class Person {
//成员变量
private String name;
//2. 类该有的成员(成员变量、方法、构造器)抽象类都可以有。
public Person() {
}
public Person(String name) {
this.name = name;
}
//成员方法
public void eat(){
System.out.println("吃饭");
}
//需求2: 将run方法修改为抽象方法
public abstract void run();
//Abstract methods cannot have a body
}
//4. 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
//
class Student extends Person{
@Override
public void run() {
System.out.println("学生跑得快");
}
}
abstract class Teacher extends Person{
}
1.3总结
2.抽象类的使用场景
多个类中只要有重复代码(包括相同的方法签名),都应该抽取到父类中去。
此时父类中就可能存在只有方法签名的方法,此时父类必定是一个抽象类。
抽出这样的抽象类是为了更好地支持多态。
/*
抽象类的应用场景和好处
1、将所有子类中重复的代码,抽取到抽象的父类中,提高了代码的复用性(先编写子类,再编写抽象类)
2、我们不知道系统未来具体的业务时,可以先定义抽象类,将来让子类去继承实现,提高了代码的扩展性 (先编抽象类,再编写子类)
需求
某宠物游戏,需要管理猫、狗的数据。
猫的数据有:名字;行为是:喵喵喵的叫~
狗的数据有:名字;行为是:汪汪汪的叫~
*/
public class Demo {
public static void main(String[] args) {
//创建猫(多态)
Animal cat = new Cat();
cat.setName("加菲猫");
cat.cry();
//创建狗(多态)
Animal dog = new Dog("赵志开");//父类构造器赋值
//dog.setName("赵志开");
dog.cry();
}
}
//父类构造器赋值
//动物(父类)
abstract class Animal {
private String name;
public Animal() {
}
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//父类:只做方法的声明(方法签名)
//抽象方法:子类必须重写方法(重新实现方法)
public abstract void cry();
}
//猫
class Cat extends Animal {
public Cat() {
}
public Cat(String name) {
super(name);
}
@Override
public void cry() {
System.out.println(getName()+"喵喵喵");
}
}
//狗
class Dog extends Animal {
public Dog() {
}
public Dog(String name) {
super(name);
}
@Override
public void cry() {
System.out.println(getName()+"汪汪汪");
}
}
3.模板方法设计模式
设计模式:前人对某一些、某一类问题总结出来的一些套路。
模式:前人总结好的解决某一类问题的套路。
Java中共有23种设计模式,常用约5-6种,比如模板方法设计模式。
模板方法设计模式解决的问题是:
一个功能的完成需要经过一系列步骤,这些步骤是固定的,但是中间某些步骤具体行为是待定的,在不同场景中行为不同。
模板方法设计模式
1.定义一个抽象类(父类)提供模板方法。
2.模板方法中,需要让子类自己实现的地方,定义为抽象方法。
3.子类只需要继承该抽象类,重写抽象方法即可完成想完成的功能。
package com.itheima.h_abstract_模板方法设计模式;
/*
设计模式
对于某类问题,前人总结出类的解决问题的套路
模板方法设计模式
一个功能的完成需要经过一系列步骤,这些步骤是固定的,但是中间某些步骤具体行为是待定的,在不同的场景中行为不同
使用思路
1、定义一个抽象类(Person作为父类),提供模板方法
2、模板方法中,需要让子类自己实现的地方,定义为抽象方法
3、子类(Teacher Student)只需要继承该抽象类,重写抽象方法即可完成些完成的功能
多学一招:
建议使用final关健字修饰模板方法
模板方法是给对象直接使用的,不能被子类重写
一旦子类重写了模板方法,模板方法就失效了
*/
public class Demo {
public static void main(String[] args) {
//创建老师的对象,调用模板方法
Person p = new Teacher();
p.work();
}
}
//1.定义一个抽象类(Person作为父类),提供模板方法
abstract class Person{
public void work(){
//1.吃早饭
System.out.println("吃早饭");
//2.不同细节的部分弄成抽象方法来调用
doSomething();
//3.吃午饭
System.out.println("吃午饭");
}
//2.模板方法中,需要让子类自己实现的地方,定义为抽象方法
public abstract void doSomething();
}
//3.子类(Teacher Student)只需要继承该抽象类,重写抽象方法即可完成些完成的功能
class Teacher extends Person{
@Override
public void doSomething() {
System.out.println("讲课");
}
}
class Student extends Person{
@Override
public void doSomething() {
System.out.println("学习");
}
}
建议使用final关键字修饰模板方法(用final修饰的方法不能被重写),在上述代码中模板方法是work()。
● 模板方法是给对象直接使用的,不能被子类重写。
● 一旦子类重写了模板方法,模板方法就会失效。
总结:
四、接口
在JDK9之后,接口和抽象类的差异已经很小,用接口更多,抽象类少一些。
1.接口概念
Java提供了关键字interface,用这个关键字可以定义一个特殊的结构:接口。
接口和类是同级的。
public interface 接口名{
成员变量(接口中的成员变量都是常量,默认被public static final修饰)
成员方法(接口中的成员方法都是抽象方法,默认被public abstract修饰)
注意:接口中不能有构造方法(即不能创建对象)和代码块
}
使用接口的注意事项:
1.接口不能直接创建对象。
2.接口是用来被类实现(implements)的,实现接口的类称为实现类。
3.一个类可以实现多个接口,实现类实现多个接口必须重写完全部接口的全部抽象方法,否则实现类需要定义为抽象类。
修饰符 class 实现类 implements 接口1,接口2,接口3,...{
}
package com.itheima.i_interface_入门;
/*
接口
Java提供了一个关键字interface,用这个关键字我们可以定义出一个特殊的结构:接口
定义格式
public interface 接口名 {
成员变量(接口中的成员变量都是常量, 默认是被public static final修饰的)
成员方法(接口中的成员方法都是抽象方法, 默认是被public abstract修饰的)
注意: 接口中不能有构造方法和代码块
}
注意事项
1. 接口不能直接创建对象
2. 接口是用来被类实现(implements)的,实现接口的类称为实现类。
3. 一个类可以实现多个接口,实现类实现多个接口,必须重写完全部接口的全部抽象方法,否则实现类需要定义成抽象类。
修饰符 class 实现类 implements 接口1, 接口2, 接口3 , ... {}
*/
public class Demo {
public static void main(String[] args) {
//1. 接口不能直接创建对象
//InterA a = new InterA();//'InterA' is abstract; cannot be instantiated
//2.需要使用实现类创建对象
InterAImpl interA = new InterAImpl();
interA.run();
//多态方法
InterA a = new InterAImpl();
//编译看左边,运行看右边
a.run();
InterB b = new InterAImpl();
b.run2();
}
}
//定义一个接口
interface InterA {
// 成员变量(接口中的成员变量都是 常量 , 默认是被public static final修饰的)
String name = "HAHA";
//成员方法(接口中的成员方法都是 抽象方法 , 默认是被public abstract修饰的)
public abstract void run();//Modifier 'abstract' is redundant for interface methods
//注意:接口中不能有 构造方法 和 代码块
}
interface InterB{
void run2();
}
//2.接口是用来被类实现(implements)的,实现接口的类称为实现类。
//(接口---实现类)找到接口,接口无法单独实现功能,要找到接口的实现类
//3. 一个类可以实现多个接口,实现类实现多个接口,必须重写完全部接口的全部抽象方法,否则实现类需要定义成抽象类。
//修饰符 class 实现类 implements 接口1, 接口2, 接口3 , ... {}
class InterAImpl implements InterA,InterB{
@Override
public void run() {
}
@Override
public void run2() {
}
}
总结:
2.接口的好处
2.1面向接口编程
接口类:
//1.接口:声明方法
//2.定义接口实现类:实现方法的真正功能(重写方法)
public interface InterC {
void save();
}
接口实现类:
public class InterCImpl implements InterC{
@Override
public void save() {
System.out.println("保存成功了");
}
}
测试类:
public class Test {
public static void main(String[] args) {
//3.创建InterCImpl对象,调用save()方法
InterC interC = new InterCImpl();//多态的形式
interC.save();
}
}
回归到多态的概念,多态是在继承或者实现下的一种现象。
继承是类与类之间,实现是类与接口之间。
2.2多态的两种形式
1.父类类型 变量名 = new 子类();
2. 接口 变量名 = new 实现类();
2.3引入接口的原因
改动右边代码会影响左边代码,此时两边的类耦合(紧密联系)。
引用接口可以实现解耦合,在接口中只做方法的声明,也就是规定。规定好业务中只有两个方法,register(注册)和login(登录)。
当在接口中做好规定之后,前端小妹妹只需要关注接口中的内容即可。
接口类:
public interface UserService {
//方法的声明 规定
//注册
void register();
//登录
void login();
}
测试类:
/*
接口特点、好处
让程序可以面向接口编程,这样程序员就可以灵活方便的切换各种业务实现。(解耦合)
*/
//使用这个类 模拟一个调用这角度的类
public class Demo {
public static void main(String[] args) {
//在这里调用用户的注册和登录方法
//1.创建对象
/*UserServiceImpl userServiceImpl = new UserServiceImpl();
userServiceImpl.register();
userServiceImpl.login();*/
UserService userService = new UserServiceImpl();
userService.register();
userService.login();
}
}
如上,按照接口,前端小姐姐的活已经做完了。
后端写代码时也要严格按照接口来写,后端写的业务类必须实现(implements)接口,要重写接口中的方法。
接口实现类:
//这是一个用户操作类,可以完成用户的注册、登录功能
public class UserServiceImpl implements UserService{
@Override
public void register() {
//3s
}
@Override
public void login() {
//5s
}
}
此时,后端换人重新写接口实现类,前端小姐姐只需要修改创建接口实现类的语句。
像这样,虽然后端代码改动了,但是对前端小姐姐的代码影响不大,通过引入接口完成了解耦合。
此时,这两个类的耦合仍然存在,但不大,想要完全解耦合需要等到学完框架才能实现。
2.4接口的好处:
有了接口之后,调用的一方只关注接口里的方法;
而实现的一方要强行按照接口里定义的内容来写。
在2.3中,调用的一方是Demo类,接口实现类中有哪些方法就用那些方法。
而实现类中要求实现接口,不实现就不能用。
1.让程序员可以面向接口编程,这样程序员可以灵活方便地切换各种业务实现(解耦合)。
(帮助理解:生活中面向接口编程的例子:
1.电脑电源线插排有国标,标准就是接口,接口的作用就是定标准的(一旦标准订好了,实现类就按标准去实现)。
2.老师上课也是一种面向接口的思想,第一个老师上课的时候打开共屏开始讲课,切换一个老师过来,还是用相同方式讲课,对听课的学生本人来说影响不大。
但如果不是这样,第一个抱着小黑板来上课,第二个意念上课,此时每一次切换老师对学生本人影响很大。
在这个例子中,这里的接口是公司规定的上课方式,上课方式规定好之后,学生按照规定听课,老师按照规定讲课,就可以实现切换老师和学生互不影响,实现解耦合。
)
3.JDK8开始,接口新特性:新增三种方法
public interface 接口名{
成员变量(接口中的成员变量都是常量,默认被public static final修饰)
成员方法(接口中的成员方法都是抽象方法,默认被public abstract修饰)
注意:接口中不能有构造方法(即不能创建对象)和代码块
}
在之前学的接口定义中,接口中的方法都是抽象方法
//1、默认方法(jdk8开始支持):对接口中的方法提供==默认实现==
//使用default修饰,有方法体,可以但是不强制要求实现类重写, 只能通过实现类的对象调用
//默认方法和原来相比较有两个地方不一样,第一个就是加了default关键字在返回值之前,第二个是方法有实现,叫做默认实现,它的好处是有方法一旦有方法体就不会强制接口的实现类必须重写此方法,有点接近抽象类,抽象类中有抽象方法和非抽象方法。
default void test1(){
...
}
//2、静态方法(jdk8开始支持):方便调用
//使用static修饰,有方法体,只能通过接口名调用(叫静态所以可以通过接口名.方法名调用)
static void test2(){
...
}
// 3、私有方法(jdk9开始支持):提高代码复用性(做抽取)
//使用private修饰,服务于接口内部,用于抽取相同的功能代码
private void test3(){
...
}
在接口中默认实现,不用再在实现类中实现了
调用接口中的静态方法
接口名.方法名
将动物苏醒了打印语句抽取出来,节省代码
在JDK中和很多程序源代码中,有很多接口新特性的应用,但在自己定义接口时很少使用,还是用原来的默认形式来定义。
4.接口的多继承、使用接口的注意事项(了解)
类和接口的关系总结:
1.类和类:继承(extends)关系,对于类,只支持单继承,不支持多继承,但是可以多层继承。
2.接口和接口:继承(extends)关系,对于接口,支持多继承。
3.类和接口:实现(implements)关系,支持多实现,一个类同时实现多个接口。
对于类,不支持多继承;但是对于接口,支持多继承。
接口的多继承:
一个接口可以继承多个接口,所以说接口是支持多继承的,这样做是为了方便类去实现。
public interface C extends B,A{
}
使用接口的注意事项:(了解)
1.一个接口继承多个接口,若多个接口中存在方法签名冲突,则不支持多继承。
2.一个类实现多个接口,若多个接口中存在方法签名冲突,则不支持多实现。
3.一个类实现多个接口,多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。
4.一个类继承了父类,同时又实现了接口,父类中和接口中有同名的默认方法,实现类会优先用父类的。
五、常量
1.常量的概念
● 使用了static final修饰的成员变量称为常量;
static修饰的变量是静态变量,可以通过类名.变量名访问;若是在接口中,可以用接口名.变量名访问。
用final修饰,只能被赋值一次,不能修改。
2.常量的作用
● 常量的作用:通常用于记录系统的配置信息。
public class Constant{
public static final String SCHOOL_NAME = "传智教育";
}
用static修饰,可以直接Constant.SCHOOL_NAME,用public修饰,任意位置都可以使用。
注意:常量名的命名规范:建议使用大写英文单词,多个单词之间使用下划线连接起来。
程序编译后,出现常量的地方全部会被替换成其记住的字面量,这样可以保证使用常量和直接用字面量的性能是一样的。
查看代码编译后结果的方法:
在当前工程文件夹的out文件夹中查看.class文件
/*
常量:
使用了static final修饰的成员变量就被称为常量, 通常用于记录系统的配置信息。
命名规范:
单词全部大写,多个之间使用_连接
优点:
1. 代码可读性更好,可维护性也更好。
2. 程序编译后,出现常量的地方全部会被替换成其记住的字面量,这样可以保证使用常量和直接用字面量的性能是一样的。
*/
public class Demo {
public static void main(String[] args) {
//需求2: 保存学生的性别, 为提高性能,经常使用0和1表示性别
//变量String gen = "男";
//int gen = 1;
int gen = Constant.MAN;
System.out.println(gen);
//男 1
//女 0
//编写.java——>编译.class——>执行
}
}
class Constant {
//需求1: 定义一个常量,记录学校的名称
public static final String SCHOOL_NAME = "黑马程序员顺义校区";
//提前定义两个变量
public static final int MAN = 1;
public static final int WOMEN =0;
}
使用常量是为了增加代码的可读性。
3.使用常量记录系统配置信息的优势和执行原理
1.代码可读性更好,可维护性更好。
2.程序编译后,出现常量的地方全部被替换为其记住的字面量,这样可保证使用常量和直接使用字面量的性能是一样的。
定义常量要用 static final 修饰
调用常量直接用类名.常量名
六、枚举
1.什么是枚举
● 枚举是一种特殊的类,常用于简洁地标识一些固定的值
枚举的格式:
修饰符 enum 枚举类名{
枚举项1,枚举项2...;
其他成员...
}
public enum Sex{
MAN,WOMEN;
...
}
2.枚举的注意事项:
● 枚举类中的第一行,只能写一些合法的标识符(名称),多个名称用逗号隔开。
● 这些名称(枚举项),本质是常量,每个常量都会记住枚举类的一个对象。
这个常量指向本类的对象
要验证本结论,需要看编译后的字节码文件。因为直接看枚举类的话,就只能看到enum Sex{ MAN,WOMEN; }
,看不到常量之类的定义,所以要编译(javac)后再反编译(javap)回来。
如上图可以看到,编译之后的字节码文件只增加了一个私有构造方法。
所以idea原生自带的反编译插件不能实现,得用jdk自带的反编译。
如下图,即为jdk的反编译结果。
反编译得到的结果:
final class Sex extends java.lang.Enum<Sex> {
public static final Sex MAN = new Sex();//指向当前类的一个对象
public static final Sex WOMEN;
public static Sex[] values();
public static Sex valueOf(java.lang.String);
static {};
}
可以看出,枚举本质上是个类,此类被final修饰,不能被继承。
此类的父类是java.lang.Enum,是jdk提供的。
使用不同的反编译工具,得到的结果也不同,以下是比较全的结果。
定义了两个常量MAN和WOMEN,每个常量都指向了当前类的对象。
也即前面所说的,这些枚举项的本质是常量,每个常量都指向本类的对象。
常量能解决手抖不小心写错的问题,不能解决精心设计写错的问题。
此时就要引入枚举。
3.枚举的特点
1.枚举类的第一行只能罗列一些名称,这些名称都是常量,且每个常量记住的都是枚举类的一个对象。
2.枚举类的构造器都是私有的(不管写不写都只能是私有的),不能改成用public修饰,因此枚举类对外不能创建对象。
3.枚举都是最终类,不能被继承。
4.枚举类中,从第二行开始,可以定义类的其他各种成员。
5.编译器为枚举类新增了几个方法,并且枚举类都继承java.lang.Enum类,从enum类也会继承到一些方法public static Sex[] values(); public static Sex valueOf(java.lang.String);
这两个方法都用static修饰,可以直接用类名.方法名调用
public static Sex[] values();
返回值是Sex[],返回结果是枚举类中有哪些项。
public static Sex valueOf(java.lang.String);
此方法时通过字符串找到对象
瞎传字符串,匹配不到就会报错:非法参数异常
从jdk5开始,switch支持枚举
//枚举在switch语句中出现
Sex sex = Sex.MAN;
switch(sex){
case MAN:
System.out.println("男");
break;
case WOMEN:
System.out.println("女");
break;
}
import java.util.Arrays;
/*
枚举
Java提供的一种用于简洁表示一些固定值的类
枚举类格式
修饰符 enum 校举类名{
校举顶1,校举项2..;
其他成员..
}
枚举的特点
1、枚举类的第一行只能罗列一些名称,且默认都是常量,每个常量记住的就是枚举类的一个对象
2、枚举类的构造器都是私有的(自己提供也只能是私有的),因此枚举类对外不能创建对象
3、枚举都是最终类,不能被继承
4、枚举类中,从第二行开始,可以定义类的其他成员
5、编译器为枚举类新增了几个方法,并且所有枚举类都是java.lang.Enum的子类,所以可以使用父类的方法
*/
public class Demo {
public static void main(String[] args) {
//1. 需求: 创建Student对象,使用set赋值为:张三,MAN
Student student = new Student();
student.setName("张三");
//student.setSex("MAN");
student.setSex(Sex.MAN);
System.out.println(student);
//5、编译器为枚举类新增了几个方法,并且所有枚举类都是
// java.lang.Enum的子类,所以可以使用父类的方法
System.out.println(Arrays.toString(Sex.values()));
System.out.println(Sex.valueOf("MAN"));
}
}
class Student {
private String name;
private Sex sex;
//private String sex;
public void setSex(Sex sex) {
this.sex = sex;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
'}';
}
}
//定义枚举类
enum Sex{
//1、枚举类的第一行只能罗列一些名称,且默认都是常量,
// 每个常量记住的就是枚举类的一个对象
// public static final Sex MAN = new Sex();
MAN,WOMEN;
//4、枚举类中,从第二行开始,可以定义类的其他成员,可以写属性也可以写方法,但是一般不写,没有这个需求,如下
public void show(){}
}
常量和枚举的功能很相似。
七、总结
多态是在继承或者实现情况下的一种现象
多态的前提:
1.有继承/实现关系
2.存在父类引用指向子类对象Person p = new Student()
3.存在方式重写
day03之内部类&泛型&常用API
一、内部类
1.内部类介绍
1.内部类是类中的五大成分之一(成员变量、方法、构造器、代码块、内部类)。
2.一个类定义在另一个类的内部,这个类就是内部类。
3.一个类的内部包含一个完整的事物(比如汽车中包含了一个完整的发动机)并且这个事物没有必要单独设计(发动机没有必要脱离汽车单独存在,单独一个发动机什么都干不了,只有放在汽车里才有用)的时候,可以把这个事物设计成内部类。
public class Car{
//内部类
public class Engine{
}
}
内部类根据所处位置不同,内部类分为四种:
1.成员内部类:位于一个类中成员(成员就是成员变量和成员方法)位置的类(其实就是在类的第一层)
2.静态内部类:用static修饰的成员内部类
3.局部内部类:在方法里定义的类
4.匿名内部类[重点]:一种特殊的局部内部类,都是在方法里定义的类,但是方法不太一样,局部内部类一般在方法体中,匿名内部类一般在方法声明的时候。
目前只掌握匿名内部类即可。
2.成员内部类
定义在一个类中成员位置的类,在内部类中也可以定义成员属性和方法。
虽然叫做内部类,但本质还是类,里面也是可以有变量(属性)和方法的。
public class Outer {
//成员内部类
public class Inner{
//成员属性和方法
}
}
接下来我们研究有了一个成员内部类之后,怎么创建成员内部类的对象。
//创建对象的格式:
外部类名.内部类名 对象名 = new 外部类(...).new 内部类(...);
Outer.Inner in = new Outer().new Inner();
//相当于在一个对象的基础上再new一个对象
/*
成员内部类
义在一个类中成员位置的类
定义类的格式:
public class Outer{
//成员内部类
public class Inner{
}
}
创建对象的格式:
外部类名.内部类名 对象名 = new 外部类(..).new 内部类(..);
Outer.Inner in = new Outer().new Inner();
使用特点及注意:
1、成员内部类中可以定义实例成员,静态成员 (注意: 静态成员从JDK16开始支持)
2、成员内部类中的实例方法中,可以直接访问外部类的实例成员,静态成员
3、如果内部和外部类出现了重名的成员,可以通过(外部类名.this.xxx) 强行访问外部类的成员
*/
public class Demo {
public static void main(String[] args) {
//如下是内部类最基本的一个用法
//先定义,然后创建对象,调方法
//外部类名.内部类名 对象名 = new 外部类(..).new 内部类(..);
Outer.Inner in = new Outer().new Inner();
in.show();
System.out.println(in.age);
}
}
//外部类
class Outer {
public static String name = "outer";
public int age = 19;
//内部类
public class Inner {
//属性
//1、成员内部类中可以定义实例成员,静态成员 (注意: 静态成员从JDK16开始支持)
public static String name = "inner";//从JDK16开始才能写静态成员
public int age = 18;
//方法
public void show() {
System.out.println("内部类");
//2、成员内部类中的实例方法中,可以直接访问外部类的实例成员,静态成员
System.out.println(name);
System.out.println(age);
//3、如果内部和外部类出现了重名的成员,可以通过(外部类名.this.xxx) 强行访问外部类的成员
System.out.println(Outer.this.name);
System.out.println(Outer.this.age);
}
}
}
成员内部类中可以定义实例成员和静态成员,静态成员从JDK16开始支持,也即在JDK16之前,成员内部类中只能定义实例成员。
可以通过如下方式验证:
File——>Project Structure——>Project Settings——>Project——>Project language level——>把校验版本调成15——>编译不通过,报错,所以版本一定要调到16,不支持15
成员内部类中的实例方法中可以直接访问外部类的实例成员和静态成员,in.name,此时idea没有代码自动补全提示,因为name是静态成员变量,推荐使用 类名.静态成员变量名 访问,当然用成员内部类的对象直接访问也可以。
如果运行时出现问题,说明idea的Java编译器版本选择不正确,Java的项目字节码版本不正确。
File——>Settings——>Build,Execution,Deployment——>Complier————>Java Compiler——>Project bytecode version调成16——>Apply——>OK
之后就可以正常运行。
总结:
3.静态内部类
3.1静态内部类的定义
用static修饰的成员内部类
静态内部类和成员内部类很相似,知识多了一个static来修饰。
public class Outer{
//静态内部类
public static class Inner{
}
}
3.2静态内部类创建对象的格式:
外部类名.内部类名 对象名 = new 外部类.内部类(...);
Outer.Inner in = new Outer.Inner();
3.3静态内部类中访问外部类成员的特点
可直接访问外部类的静态成员,不可以直接访问外部类的实例成员。
/*
静态内部类
使用static修饰的内部类
定义类的格式:
public class Outer{
public static class Inner{
}
}
创建对象的格式:
外部类名.内部类名 对象名 = new 外部类(..).内部类(..);
Outer.Inner in = new Outer().Inner();
特点:
1. 可以直接访问外部类的静态成员
2. 不可以直接访问外部类的实例成员
*/
public class Demo {
public static void main(String[] args) {
//创建对象
//外部类名.内部类名 对象名 = new 外部类.内部类(..);
Outer.Inner in = new Outer.Inner();
in.show();
}
}
//外部类
class Outer{
public static String name = "outer";
public int age = 18;
//静态内部类
public static class Inner{
public void show(){
System.out.println("静态内部类");
System.out.println(name);
//System.out.println(age);
}
}
}
3.4总结
4.局部内部类
在方法中定义的类
局部内部类定义在方法中、代码块中、构造器等执行体中。
public class Test{
public static void main(String[] args){
}
public static void go(){
class A{
}
abstract class B{
}
interface C{
}
}
}
5.匿名内部类(重点)
一种特殊的局部内部类
所谓匿名是指程序员不用为这个类声明名字
匿名内部类是用来简化抽象类和接口的
new 类或接口(参数值...){
方法实现(){}
}
new Animal(){
@Override
public void try(){
}
}
二、泛型
1.泛型的概念
2.泛型类
泛型类
修饰符 class 类名<类型变量,类型变量,...>{
}
/*
泛型类
在定义类的时候设置泛型, 然后在后续方法上使用
格式
修饰符 class 类名<类型变量,类型变量,…> {
}
注意:
类型变量建议用大写的英文字母,常用的有:E、T、K、V 等
*/
public class Demo {
public static void main(String[] args) {
//创建一个MyList,需要指定泛型
MyList<String> list = new MyList<>();
//调用add
list.add("abc");
list.add("12345");
//查询
String s = list.get(0);
System.out.println(s);
}
}
//自定义一个泛型类, 模仿ArrayList的add和get功能
//E:代表的是暂时不确定List中存储的元素类型,
//当使用者创建这个List集合的时候再去规定类型
class MyList <E>{
//准备一个数组来装载元素
Object[] arr = new Object[9999];
//E[] arr = new E[9999];
//Type parameter 'E' cannot be instantiated directly
//准备一个计数器代表索引
int count = 0;
//新增 add
public void add(E e){
arr[count++] = e;
}
//查询 get
public E get(int index){
return (E)arr[index];
}
}
3.泛型接口
三、常用API
Object类:
Object类是Java中所有类的祖宗类。
所以Java中所有类的对象都可以直接使用Object类中提供的一些方法。
Object类的常见方法:
toString
包装类:
● 包装类就是把基本类型的数据包装成对象
StringBuilder
● StringBuilder代表可变字符串对象,相当于一个容器,里面装的字符串可变,用来操作字符串。
● StringBuilder比String更适合进行字符串的修改操作,效率更高,代码也更简洁。
StringJoiner
使用StringJoiner的原因
day04之常见API&日期时间&Arrays
一、常用API
1.Math
代表数学,是一个工具类(里面都是静态方法,用Math.方法名调用即可),提供对数据进行操作的一些静态方法。
2.System
3.Runtime
4.BigDecimal
BigDecimal用于解决浮点型运算时,出现结果失真的问题。
二、jdk8之前传统的日期、时间
1.Date
代表日期和时间。
2.SimpleDateFormat
代表简单日期格式化,可把日期对象、时间毫秒值格式化成想要的形式。
1.创建格式化器对象(指定格式) sdf
2.时间——>字符串
字符串 sdf.format(时间)
3.字符串——>时间
时间 sdf.parse(字符串)
3.Calendar
三、jdk8开始新增的日期、时间
1.学习JDK8新增时间的原因
不推荐使用:
推荐使用:
LocalDate:年、月、日
LocalTime:时、分、秒
LocalDateTime:年、月、日、时、分、秒
ZoneId:时区
ZoneDateTime:带时区的时间
DateTimeFormatter
2.LocalDate
代表本地日期(年、月、日、星期)
LocalDate的常用API(都是处理年、月、日、星期相关的)
3.LocalTime
代表本地时间(时、分、秒、纳秒)
4.LocalDateTime
代表本地日期、时间(年、月、日、星期、时、分、秒、纳秒)
它们获取对象的方案
ZoneId时区的常见方法
soft-wrap
四、Arrays
1.Arrays简介
● 用来操作数组的一个工具类
2.Arrays类提供的常见方法
如果数组中存储的是对象,如何实现排序。
此时需要知道排序规则,比如按照年龄排,按照身高排。
public static void main(String[] args) {
//1. 定义学生数组对象,保存四个学生进行测试
Student[] students = new Student[4];
students[0] = new Student("蜘蛛精", 169.5, 23);
students[1] = new Student("紫霞", 163.8, 26);
students[2] = new Student("紫霞", 163.8, 26);
students[3] = new Student("至尊宝", 167.5, 24);
//2. 指定排序的规则
Arrays.sort(students);//ClassCastException类型转换异常 不能将Student转换成java.lang.Comparable
//也就是说如果传进sort()方法的东西能排序,那这种类型必须是带比较器的,如果不带比较器,不知道怎么比较
//3. 打印结果
System.out.println(Arrays.toString(students));
}
指定排序规则有两种方式:
方式一:自然排序:让需要排序的对象的类实现Comparable(比较规则)接口,也就是说如果要排序,你的类必须是Comparable接口的子类。(如果不是这个接口的子类就不能排序)
也即如果想排序就让1.需要排序的对象实现Comparable接口。
2.然后重写compareTo方法。
3.制定比较规则。
如果要实现自然排序,需要:
1.让我们要排序的类,也就是Student,实现一个Comparable接口,并且还要指定泛型,这个泛型就是Student本身。
//1.实现Comparable接口,指定泛型
class Student implements Comparable<Student> {
...
}
2.重写compareTo方法。
alt+insert
3.指定排序规则。
//2.重写compareTo方法
@Override
public int compareTo(Student o) {//底层是一个循环,看到的本方法知识循环中的一次
//3.指定排序的规则(按照年龄 正序)
//当前元素(即将放入数组) this
// o 已经在数组里面的
//返回正数,表示当前元素较大——>Java会把this对象放到比较对象的右边
return this.age - o.age;
/*if(this.age>o.age){
return 1;//这是一个约定,只要是整数就放比较对象右边
}*/
//返回负数,表示当前元素较小——>Java会把this对象方法比较对象的左边
/*if (this.age < o.age) {
return -1;
}*/
//返回0,表示相同
/*
return 0;*/
}
在重写的方法compareTo方法里面制定排序规则。
这个排序规则是我们和Java定好的一个约定,只要按照Java的规定做,出来的就是正序。
如果想要正序,要比较两个元素(当前元素this(是即将要放入数组的,由于在Student类里面,可以用this取到)和Student o(表示的是已经在数组中的))。
大家可能会有疑问,已经在数组中的怎么只有一个,不应该是数组里面有好多,往进放一个吗。
其实在底层它是一个循环,能看到的只是其中的一次(外层是套了一个循环的)。
compareTo()方法的返回值是int :
返回正数,表示当前元素较大——>Java会把this对象放到比较对象的右边。
返回负数,表示当前元素较小——>Java会把this对象方法比较对象的左边。
使用自然排序法的方式排序有一个问题:需要让需要排序的对象的类实现Comparable(比较规则)接口,也就是说,在使用自然排序方式的时候,其实是改了原来的Student对象的。
若假设Student对象不是自定义的,而是原来在jar包里的东西(Java提供的,Java提供的东西是无法修改的,File is read-only),
有时候类不是自己写的,没法加东西(比如实现接口),那就无法用上述第一种方式。
自然排序法要应用有一个条件,就是要排序的对象所对应的实体类得能修改,如果没有修改该类的权限,第一种方式就用不了了,此时就要用到第二种方式,比较器排序。
方式二:比较器排序:使用sort方法,创建Comparator比较器接口的匿名内部类对象,制定比较规则。
1.调用sort排序方法,参数二传递Comparator接口的实现类对象(用匿名内部类实现)
2.重写compare方法
3.制定排序规则
第一个参数T[] arr :是一个数组
第二个参数Comparator<?super T>c :是一个接口 ,接口里面用来实现排序规则
//1.假定Teacher类是不能修改的
//2. 指定排序的规则
//比较器排序
Arrays.sort(teachers, new Comparator<Teacher>() {
@Override
public int compare(Teacher o1, Teacher o2) {
//o1表示即将放入数组的元素
//o2表示已经在数组中的每个元素
return o1.getAge()-o2.getAge();
}
});
总结:
比较器排序功能更加强大,无论待排序的对象的实体类是否可以修改,都可以用比较器。
用自然排序的时候,待排序的对象的实体类必须是可以修改的。
自定义排序规则的时候,需要遵循的官方约定如下:
day05之异常&Lambda&&方法引用&常见算法&正则表达式
一、异常
1.异常的概念
异常代表程序出现的问题
int[] arr = {10,20,30};
System.out.println(arr[3]);//数组越界异常
System.out.println(10/0);//运算异常
2.异常的处理
3.自定义异常
3.1使用自定义异常的原因
Java无法为所有的问题都提供异常类来代表,若程序员自己写的代码中有某种问题,
想通过异常来表示,以便用异常来管理该问题,就需要自定义异常类。
3.2自定义异常的种类
3.2.1自定义运行时异常
1.定义一个异常类继承 RuntimeException。
2.重写构造器。
3.通过throw new 异常类(xxx)来创建异常对象并抛出。
编译阶段不报错,提醒不强烈,运行时才可能出现。
在上述代码中,main()方法是调用者角色,setAge()方法是被调用者角色。
调用者传入一个参数300,若发生异常,直接打印"参数不在正常年龄范围内"。
打印之后,被调用者显然知道这个异常的存在,但是调用者并不知道,因为被调用者setAge()方法并没有返回东西。
(帮助理解:有人给你赋值,赋的这个值不对,你自己知道,但不告诉给你赋值的人,那就没什么用,下次还是有可能赋错误的值)
当前上述代码直接运行,有输出,但是只是在setAge()方法中有所输出,并没有给调用者main()方法任何反馈。
作为调用者来说,其实并拿不到异常的结果。
现在想要拿到这个异常结果,就可以采用异常。
我们知道,异常一旦发生之后,如果不处理,就会往上抛,抛回到调用处,就认为调用者main()方法知道了异常的存在。
所以可以把异常理解为一种特殊的返回值,不需要明确声明,调用者就可以接收的到。
那么在上述代码中,我们需要在setAge()方法返回一个异常给调用者main()方法,要返回一个年龄越界异常出去,但是Java和JVM中没有提供这个异常,此时就需要自定义异常。
自定义一个年龄越界异常
public class Demo4 {
public static void main(String[] args) {
try {
setAge(300);
}catch (Exception e){
System.out.println(e.getMessage());
}
}
//区分两个关键字
//throws:用在方法声明上 作用:声明当前方法可能抛出的异常
//throw:用在方法里面 作用:真正的抛出一个异常
//设置年龄的方法
public static void setAge(int age) {
if (age < 0 || age > 200) {
//System.out.println("参数不在正常年龄范围内");
//异常其实是一种特殊的返回值,以便通知上层
//自定义一个年龄越界异常
//3.在需要抛出异常的地方使用throw关键字
//抛出异常类的对象
//throw new AgeOutOfBoundsRunTimeException();
throw new AgeOutOfBoundsRunTimeException("年龄应该在0-200之间");
} else {
System.out.println("赋值成功");
}
}
}
//自定义运行时异常 需要:
// 1.自定义一个类 继承RuntimeException
class AgeOutOfBoundsRunTimeException extends RuntimeException{
//2.在类中提供构造函数
public AgeOutOfBoundsRunTimeException() { super(); }
public AgeOutOfBoundsRunTimeException(String message) { super(message); }
}
可以看到,此时调用者main()方法已经接收到了返回结果
3.2.2区分两个关键字throws和throw
throws:用在方法声明上 作用:声明当前方法可能抛出的异常
throw:写在方法里 作用:真正地抛出一个异常
//在需要抛出异常的地方使用throw关键字 抛出异常类的对象
throw new AgeOutOfBoundsRuntimeException();//抛出异常在new的时候没有传参,相当于调用无参构造
//调用无参构造,返回的异常中没有具体信息,只提示了异常类型是年龄越界异常
//对于业务性的操作,传一个年龄越界异常,不知道应该传的age的范围,在自定义异常时,经常会给用户返回一些信息
//抛出异常在new的时候没有传参,相当于调用无参构造
//调用无参构造,返回的异常中没有具体信息,只提示了异常类型是年龄越界异常
//对于业务性的操作,传一个年龄越界异常,不知道应该传的age的范围,在自定义异常时,经常会给用户返回一些信息(如:s is null)。
此时需要用到第二个构造函数,这个函数允许传一个字符串类型(String)的message,这个message就是用来封装错误信息的。比如如下代码片:
throw new AgeOutOfBoundsRuntimeException("年龄应该在0-200之间");
此时返回异常时还带着异常的信息
假设已知可能发生异常的位置,就可以进行try catch操作
发起请求(setAge)——>出现异常(年龄越界)——>返回调用位置——>异常被捕获(catch)——>打印异常信息(message)
3.2.3自定义编译时异常
自定义编译时异常和运行时异常只有一个区别,就是在定义异常类时继承的类不同
1.定义一个异常类继承Exception
2.重写构造器
3.通过throw new 异常类(xxx)来创建异常对象并抛出。
编译阶段就报错,提醒更加强烈。
/*
自定义异常
Java无法为这个世界上全部的问题都提供异常类来代表, 如果以后我们自己写的代码中的某种问题,
想通过异常来表示,以便用异常来管理该问题,那就需要自己来定义异常类了。
自定义编译时异常
1、定义一个异常类,继承Exception
2、在类中提供构造函数
3、在需要抛出异常的地方使用throw关键字 抛出异常类的对象
注意
编译时异常,在编写代码的过程中必须手动处理
异常的作用
1、异常是用来查找系统bug 的关键参考信息
2、异常可以作为方法内部一种特殊的返回值,
以便通知上层调用者,代码底层的执行情况
*/
public class Demo5 {
public static void main(String[] args) throws AgeOutOfBoundsException {
setAge(300);
}
//设置年龄的方法
public static void setAge(int age) throws AgeOutOfBoundsException {
if (age < 0 || age > 200) {
//System.out.println("参数不在正常年龄范围内");
//3.在发生异常的位置用throw new AgeOutOfBoundsException("年龄范围在0-200之间");返回一个异常
throw new AgeOutOfBoundsException("年龄范围在0-200之间");
} else {
System.out.println("赋值成功");
}
}
}
//自定义编译时异常
//1.定义一个异常类,继承Exception
class AgeOutOfBoundsException extends Exception{
//2.在类中提供构造函数
public AgeOutOfBoundsException() {
}
public AgeOutOfBoundsException(String message) {
super(message);
}
}
对于编译时异常,该方法要么try...catch...
捕获,要么明确地抛出,因此可以alt+enter
,直接throws AgeOutOfBouondsException
抛出异常。
在main方法中,一样既可以try…catch,也可以throws抛出。
此时运行结果如下:
3.2.4运行时异常和编译时异常的区别
1.继承的类不同
2.如果抛出的是一个编译时异常,方法必须明确声明
3.2.5异常的作用
1.查询系统Bug
2.异常可以作为方法内部的一种特殊的返回值,在上层调用的时候,调用方法返回一个异常,其实就是返回一种执行结果。
总结:
二、lambda表达式
三、常用算法
四、正则表达式
正则表达式是由一些特定的字符组成的,代表的是一个规则,一般根据
day06之集合->Collection&List&Set
ArrayList和数组对比的学习:
集合是一种容器,用来装数据,类似于数组,但集合大小可变,开发中很常用。
在Java体系中,为了满足不同的业务场景需求,Java提供了很多不同特点的集合
一、集合
1.集合体系概述
1.1集合体系结构
集合在大方向上分为两个体系:collection和map
● collection:单列集合,每个元素(数据)只包含一个值。
● map:双列集合(key-value集合),每个元素包含两个值(键值对)。
1.2Collection集合体系
1.2.1Collection集合体系继承图谱:
单列集合中的顶级接口是Collection
List和Set继承于Collection,它们也是接口。
我们学习它们的实现类:
List的两个实现类ArrayList和LinkedList
Set的三个实现类HashSet和TreeSet,还有HashSet 的子类LinkedHashSet。
1.2.2Collection集合特点
● List集合:有序、可重复
● List 的子类:ArrayList、LinkedList,它们完美继承了父类的特点:有序可重复
● Set集合:无序、不重复
● HashSet也完美继承了父类的特点:无序不重复
● LinkedHashSet:经过改造:存取有序(存进去的顺序就是取出来的的顺序),不重复
● TreeSet:可以排序,使用TreeSet可以自定义排序规则
/*
集合体系
|---Collection 单列集合
|---List 有序 可重复
|---ArrayList
|---LinkedList
|---Set 无序 不可重复
|---HashSet
|---LinkedHashSet 存取有序
|---TreeSet 默认升序
|---Map 双列集合
*/
public class Demo1 {
public static void main(String[] args) {
//List: 有序 可重复
//List是一个接口
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
list.add("张三");
System.out.println(list);//[张三, 李四, 王五, 张三]
System.out.println("***************");
//Set:无序 不可重复
Set<String> set = new HashSet<>();
set.add("张三");
set.add("李四");
set.add("王五");
set.add("张三");//Duplicate Set element
System.out.println(set);//[李四, 张三, 王五]
}
}
2.Collection常用方法
2.1为何要先学习Collection的常用方法
● Collection是单列集合的祖宗(顶级接口),它规定的方法(功能)是所有单列集合都会继承的。
先学习单列集合的顶级接口Collection的常用方法:
Collection是顶级接口,意味着它所有的子类都可以用它的方法,这是继承的一大特点。
只要把Collection学明白,再学List和Set,只需要学它们独有的方法即可。
2.2Collection的常用方法
/*
Collection<E> 这是单列集合的根接口
boolean add(E e) 添加元素
boolean remove(E e) 删除指定的元素 (如有重复删除第一个)
boolean contains(Object obj) 判断集合中是否包含指定元素
int size() 返回集合中元素的个数
boolean isEmpty() 判断集合是否为空
Object[] toArray() 将集合中元素存入一个对象数组并返回
T[] toArray(T[]a) 将集合中元素存入一个指定类型的数组并返回(指定数组长度)
void clear() 清空集合
void addAll(集合) 添加另外一个集合中的元素
*/
public class Demo2 {
public static void main(String[] args) {
//多态创建单列集合
Collection<String> collection = new ArrayList<>();
//boolean add(E e) 添加元素
collection.add("张三");
collection.add("李四");
collection.add("王五");
collection.add("张三");
Collection<String> collection2 = new ArrayList<>();
collection2.add("张三1");
collection2.add("李四2");
//boolean remove(E e) 删除指定的元素
// (如有重复删除第一个)
collection.remove("张三");
//boolean contains(Object obj) 判断集合中是否包含指定元素
boolean b = collection.contains("张三");
System.out.println(b);
boolean b1 = collection.contains("赵六");
System.out.println(b1);
//int size() 返回集合中元素的个数
System.out.println(collection.size());
//boolean isEmpty() 判断集合是否为空
System.out.println(collection.isEmpty());
//Object[] toArray() 将集合中元素存入一个对象数组并返回
Object[] objects = collection.toArray();
System.out.println(Arrays.toString(objects));//[李四, 王五, 张三]
//T[] toArray(T[]a) 将集合中元素存入一个指定类型的数组并返回(指定数组长度)
//String[] strings = collection.toArray(new String[collection.size()]);
String[] strings = collection.toArray(new String[1]);
System.out.println(Arrays.toString(strings));//[李四, 王五, 张三]
//void clear() 清空集合
collection.clear();
System.out.println(collection.isEmpty());//true
System.out.println(collection);
}
}
产生的疑问及回答:
Collection里的addAll方法:
//void addAll(集合) 添加另外一个集合中的元素
//多态创建单列集合
Collection<String> collection = new ArrayList<>();
//boolean add(E e) 添加元素
collection.add("张三");
collection.add("李四");
collection.add("王五");
Collection<String> collection2 = new ArrayList<>();
collection2.add("张三1");
collection2.add("李四2");
collection.addAll(collection2);
//T[] toArray(T[]a) 将集合中元素存入一个指定类型的数组并返回(指定数组长度)
String[] strings = collection.toArray(new String[collection.size()]);
System.out.println(Arrays.toString(strings));//[李四, 王五, 张三, 张三1, 李四2]
3.Collection遍历方式
作为单列集合,它们的顶级接口是Collection。
遍历就是循环,之前学习ArrayList时就学过循环(快捷键是fori),Collection不能用fori,因为Collection里没有get方法,get()方法是ArrayL里独有的方法,它的顶级接口Collection中没有此方法。
因此ArrayList的用get(index)的方式遍历集合的方式在此处不适用。
那么用Collection如何做遍历呢,有以下三种方式。
3.1迭代器方式
3.1.1迭代器概述
● 迭代器是用来遍历集合的专用方式(数组没有迭代器,也就是说遍历数组不能用迭代器)
迭代器是用来遍历集合的专用方式(数组没有迭代器),在Java中迭代器的代表是iterator。
3.1.2Collection集合获取迭代器的方法
用collection对象调用iterator()方法,就能得到一个迭代器对象,该迭代器对象默认指向当前集合的第一个元素。
拿到迭代器对象,可以调用它的方法:
用迭代器遍历集合的代码格式是固定的,如下:
Iterator<String> it = lists.iterator();//用当前集合获取一个迭代器对象
while(it.hasNext()){
String ele = it.next();
System.out.println(ele);
}
3.1.3迭代器执行流程
lists.iterator(),用当前集合lists获取一个迭代器对象iterator,拿到迭代器对象后,会出现一个指针,默认指向集合中的第一个元素。
先调用了hasNext方法,判断第一个位置是否是非空元素,若有元素就是true,只要是true,就通过it.next()获取此元素,当next()方法执行结束后还会进行一个操作,指针后移,之后打印此元素。
然后接着下次循环,直到while循环判断条件为false。
3.2增强for循环
增强for格式:
for(元素的数据类型 变量名:数组或者集合){
}
Collection<String> c = new ArrayList<>();
...
//c:冒号后边是集合名字,前边代表从集合中取到的每一个元素
//由于c已经限制了泛型为String,所以从中取出每一个元素都是String,s相当于临时取的一个名字
for(String s:c){
System.out.println(s);
}
● 1.既能遍历数组,也能遍历集合。
● 2.增强for循环遍历 集合 的时候,本质上是迭代器方式的简化写法。
● 3.增强for循环遍历数组 的时候,底层是普通for循环的逻辑。
/*
遍历2: 增强for循环
数组和集合都可以使用
相关格式
for(元素数据类型 变量名 : 数组或者集合){
操作变量
}
注意
1. 在增强for循环中修改数据, 是不会影响数据源的(底层会创建临时变量,来记录容器中的数据)
2. 增强for遍历集合,底层是迭代器遍历集合的逻辑
3. 增强for遍历数组,底层是普通for循环的逻辑
*/
public class Demo4 {
public static void main(String[] args) {
//1. 准备一个集合
Collection<String> collection = new ArrayList<>();
collection.add("java");
collection.add("python");
collection.add("c++");
collection.add("c#");
//2. 使用增强for循环遍历
//增强for遍历集合,底层是迭代器遍历集合的逻辑
//快捷键:collection.for
for(String s:collection){
// 1. 在增强for循环中修改数据, 是不会影响数据源的
//这里的s是遍历之后的临时的变量
// (底层会创建临时变量,来记录容器中的数据)
s = "hello";
System.out.println(s);
}
//3. 打印原来的
System.out.println(collection);
//增强for循环也可以遍历数组
int[] arr = {1,2,3};
// 3. 增强for遍历数组,底层是普通for循环的逻辑
for(int i:arr){
System.out.println(i);
}
}
}
3.3lambda表达式遍历集合
3.3.1lambda表达式遍历集合格式
lambda表达式提供了一种更简单、更直接的方式来遍历集合。
collection.forEach(new Consumer<String>(){
@Override
public void accept(String s) {
System.out.println(s);
}
});
//然后用lambda表达式简化
//collection.forEach(System.out::println);
//这句是方法引用写法
//element s可换为e
collection.forEach(s->{
System.out.println(s);
});
//此处方法体的{}不再省略,因为在forEach方法中遍历时可能进行其他操作
3.3.2案例:遍历集合中的自定义对象
/*
展示多部电影信息
每部电影都是一个对象,多部电影要使用集合装起来。
遍历集合中的3个电影对象,输出每部电影的详情信息。
*/
public class Demo6 {
public static void main(String[] args) {
//1. 创建集合
Collection<Movie> movies = new ArrayList<>();
movies.add(new Movie("《肖生克的救赎》", 9.7, "罗宾斯"));
movies.add(new Movie("《霸王别姬》", 9.6, "张国荣、张丰毅"));
movies.add(new Movie("《阿甘正传》", 9.5, "汤姆.汉克斯"));
for (int i = 0; i < movies.size(); i++) {
System.out.println(((ArrayList<Movie>)movies).get(i));
}
System.out.println("==============");
//System.out.println(movies);
//2. 遍历输出
//2-1 迭代器
Iterator<Movie> iterator = movies.iterator();
while (iterator.hasNext()) {
Movie movie = iterator.next();
System.out.println(movie);
}
System.out.println("--------------------");
//2-2 增强for
for (Movie movie : movies) {
System.out.println(movie);
}
System.out.println("--------------------");
//2-3 lambda
movies.forEach(e->{
System.out.println(e));
}
}
}
4.集合存储对象的内存图
4.1集合存储对象的内存图
4.2集合存储对象的原理
Movie实体类
运行到main()方法第一句后会创建一个movies集合,集合要指向堆内存中的一块内存区域。
这段连续的内存区域中之后存的就是电影,这段连续内存区域有一个地址,这个地址会赋值到movies上。
接下来就要给这块连续内存区域的方框中放对象,创建对象往里放的时候,每创建一个movie对象,会单独开辟一块堆内存(这块堆内存也有自己的一个地址),并不是直接把这个movie对象直接放到连续内存区域的方框中,它会把这个对象的地址保存到连续内存区域的方框中。如下图所示:
当我们查找某一个movie对象时,其实是先通过movies中保存的地址找到堆内存中那块连续的内存区域,再通过这个区域里的某一项找到具体的movie对象。
以上就是集合存储对象的原理。
总结:
对象集合中存储的是元素对象的地址
5.Collection的并发修改异常
5.1集合的并发修改异常
● 用迭代器遍历集合时,同时删除集合中的元素(边遍历、边删除),程序会出现并发修改异常错误。
● 由于增强for循环遍历集合底层就是迭代器遍历集合的简化写法。
所以使用增强for循环遍历集合,又在同时删除集合中数据,程序也会出现并发修改异常错误。
//迭代器
Iterator<String> iterator = arrayList.iterator();
//ConcurrentModificationException
//迭代器中只要是边遍历边删除就会发生并发修改异常
//在迭代器中遍历的过程中删除对象就会发生并发修改异常
while (iterator.hasNext()) {
String s = iterator.next();
//使用迭代器自己的删除方法
if (s.contains("枸杞")) {
iterator.remove();
}
/*if (s.contains("枸杞")) {
arrayList.remove(s);*/
//ConcurrentModificationException并发修改异常
System.out.println(arrayList);
}
//增强for写法
/*for (String s : arrayList) {
if (s.contains("枸杞")) {
//ConcurrentModificationException
//并发修改异常
arrayList.remove(s);
}
}*/
}
}
5.2如何保证遍历集合同时删除数据时不出bug
在基础班学习ArrayList的时候,普通for循环正着删除某些元素会出现没删掉的情况。
当时用了两种方法解决问题:
1.倒着删除
2.删除一个元素之后,索引跟着 - -
● 用迭代器遍历集合,但用迭代器自己的删除方法删除数据
(也就是说不用ArrayList的remove()方法,用迭代器对象自带的删除方法)。
while (iterator.hasNext()) {
String s = iterator.next();
//使用迭代器自己的删除方法
if (s.contains("枸杞")) {
iterator.remove();//此方法不用传参数
}
}
● 用普通for循环遍历:
可以倒着遍历并且删除;
或者从前往后遍历,但是每次删除元素后做 i - - 操作。
● 对于增强for循环遍历集合,暂时没有解决方案。
二、List集合
1.List
1.1.List系列集合的特点
有序 可重复
1.2List特有方法
Collection的所有内容List都完美继承,只需要研究List的特有方法。
List集合底层支持索引,提供了很多关于索引的方法。
当然,Collection的功能List也都继承了。
List在Collection基础上添加了一套特有的带着索引的增删改查方法。
1.3.List集合支持的遍历方式
1.迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
}
2.增强for循环
for (String s : list) {
System.out.println(s);
}
3.lambda表达式
list.forEach(e->{
System.out.println(e);
});
//list.forEach(System.out::println);
4.比Collection多一个
普通for循环(因为List集合有索引,可以用 fori )
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
1.4总结
2.ArrayList
以上已经把Collection和List里的方法和遍历方式都讲完了,Collection和List都是接口,只能创建它们的实现类的对象。
在List集合体系下,最出名的两个实现类:ArrayList和LinkedList。
面试过程中,ArrayList和LinkedList的底层数据结构很重要,面试中经常被问到。
ArrayList和LinkedList二者底层采用的数据结构不同,应用场景也不同。
2.1ArrayList集合的底层原理
● ArrayList底层基于数组实现,数组在内存中是一块连续的空间,而且每一个元素都有自己的索引,通过索引可以直接定位数组中所存储的元素。
1.利用无参构造器创建的集合,会在底层创建一个默认长度为0的数组
2.添加第一个元素时,底层会(扩容)创建一个新的长度为10的数组
3.存满时,会扩容1.5倍
4.如果一次添加多个元素,且扩容1.5倍还放不下,name新创建数组的长度以实际为准
2.2ArrayList集合的特点
ArrayList集合的特点和数组的特点有很大关系。
数组的特点:查询快、增删慢
ArrayList集合的特点:查询快、增删慢
1.查询速度快(根据索引查询数据块):查询数据通过地址值和索引定位,查询任意数据耗时相同。
查询数据直接用索引定位,不需要遍历,因此查询速度极快。
2.删除效率低:可能把后面很多数据进行前移。
删除数组中一个元素会使原来的位置空出来,这就违背了数组的定义(数组是一块连续的内存空间),此时需要元素前移。
3.添加效率极低:可能把后面很多数据后移,再添加元素,也可能需要进行数组扩容。
2.3ArrayList集合的应用场景
2.3总结
3.LinkedList
LinkedList集合的底层原理
LinkedList应用场景一:用来设计队列
队列,两端开口,先进先出,后进后出
只操作首尾
LinkedList应用场景二:用来设计栈
栈的特点:顶端开口,先进后出
只在首部增删元素,很合适
三、Set集合
无序 不可重复
Set要用到的常用方法基本就是Collection提供的,自己几乎没有额外添加新功能。
1.介绍
2.HashSet
2.1Hash值
● 哈希值本质就是一个int类型的数值,Java中每个对象都有一个哈希值。
● Java中的所有对象都可以调用Object类提供的hashCode方法,返回该对象自己的哈希值。
对象哈希值的特点:
1.同一个对象多次调用hashCode()方法返回的哈希值是相同的。
2.不同对象的哈希值一般不同,但也有可能相同(哈希碰撞)。
3.Object的hashCode方法根据“对象地址值”计算哈希值。
HashSet集合无序不可重复:
判断两个对象的标准就是两个对象的hash值是否一致
2.2底层原理
● HashSet基于哈希表实现。
● 哈希表是一种增删改查数据性能都较好的数据结构。
哈希表:
在JDK8之前,哈希表 = 数组 + 链表;
JDK8之后,哈希表 = 数组 + 链表 + 红黑树;
总结:
2.3树结构
红黑树能保证查询效率高
3.LinkedHashSet
特点:不重复、存取有序
在HashSet基础上多了一个双向链表
● LinkedHashSet是不可重复的,存取有序的,底层是基于哈希表(数组、链表、红黑树)实现的,使用双向链表记录添加顺序。
4.TreeSet
特点:
底层是基于红黑树实现的。
day07之集合->Map&Stream&递归
一、Collections
1.可变参数
1.1可变参数的概念和定义格式
可变:接收的参数的数量可以变化
参数,写在方法括号里面的内容叫做参数。
是一种特殊形参,定义在方法、构造器的形参列表里。
只有定义方法的时候才能有可变参数,调用的时候没有这种说法。
定义格式:方法名(数据类型...形参名称) { }
方法名(int... a) { }
可变参数的好处是可以灵活地接收参数(可以不接,可以接一个或多个数据,也可以接收一个数组)。
可变参数在方法内部的本质是一个数组(可以通过代码的反编译验证这一点,下面代码中用XJad进行了代码的反编译),a完全可以当数组来用,可以用增强for来遍历。
/*
可变参数
就是一种特殊形参,定义在方法、构造器的形参列表里,格式是:数据类型... 参数名称
优点
特点:可以不传数据给它;可以传一个或者同时传多个数据给它;也可以传一个数组给它。
好处:常常用来灵活的接收数据。
注意事项
1. 可变参数在方法内部就是一个数组
2. 一个形参列表中可变参数只能有一个
3. 可变参数必须放在形参列表的最后面
*/
public class Demo {
public static void main(String[] args) {
/*//准备数组,调用方法
int[] arr={1,2,3};
System.out.println(add(arr));*/
System.out.println(add(1, 2, 3, 4));
}
//可变参数
//2.一个形参列表中可变参数只能有一个
//3.可变参数必须放在形参列表的最后面
public static int add(int... a) {
int sum = 0;
//1.可变参数在方法内部就是一个数组
for (int i : a) {
sum += i;
}
return sum;
}
//计算2个整数的和
public static int add(int a, int b) {
return a + b;
}
//计算3个整数的和
public static int add(int a, int b, int c) {
return a + b + c;
}
//计算4个整数的和
public static int add(int a, int b, int c, int d) {
return a + b + c + d;
}
/*//计算n个整数的和
public static int add(int[] arr){
int sum=0;
for (int i : arr) {
sum+=i;
}
return sum;
}*/
}
//XJad反编译结果
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://kpdus.tripod.com/jad.html
// Decompiler options: packimports(3) fieldsfirst ansi space
// Source File Name: Demo.java
//package com.itheima.a_53EF53D853C26570;
//
// import java.io.PrintStream;
//
//public class Demo
//{
//
// public Demo()
// {
// }
//
// public static void main(String args[])
// {
// System.out.println(add(1, 2, 3, 4));
// }
//
// public static transient int add(int a[])
// {
// int sum = 0;
// int ai[] = a;
// int j = ai.length;
// for (int k = 0; k < j; k++)
// {
// int i = ai[k];
// sum += i;
// }
//
// return sum;
// }
//
// public static int add(int a, int b)
// {
// return a + b;
// }
//
// public static int add(int a, int b, int c)
// {
// return a + b + c;
// }
//
// public static int add(int a, int b, int c, int d)
// {
// return a + b + c + d;
// }
//}
1.2可变参数的特点及好处
特点:
1.可以不传数据给它;
2.可传一个或同时传多个数据;
3.可传一个数组给它。
好处:常用来灵活接收数据
1.3使用可变参数的注意事项
1.可变参数在方法内部本质就是一个数组
2.一个形参列表中可变参数只能有一个
3.可变参数必须放在形参列表的最后面
1.4总结
1.可变参数是一种特殊形参,定义在犯法、构造器的形参列表里。
格式:数据类型…参数名称
2.可变参数特点:
可不传数据给它,
可传一个或者同时传多个数据给它,
也可传一个数组给它。
使用可变参数的好处:常用来灵活地接收数据
3.注意事项:
1.可变参数在方法内部本质是一个数组
2.一个形参列表中可变参数只能有一个
3.可变参数必须放在形参列表的最后面
2.Collections
2.1Collections简介
Collection是单列集合根接口,本质是接口
Collections是工具类,就像Array和Array
● Collections是用来操作集合的工具类
2.2Collections提供的静态方法:
类型.方法名()即可调用
此方法也是排序,但一般用来排自定义的对象(比如自定义的Student对象)
Collections.sort(stuList);
如果直接调用,传入待排序集合,会报错。
原因是:如果传入的集合没有排序规则,必须得指定第二个参数(即自定义一个比较器传入)。
那么,怎么能知道传入的集合是否有比较规则?
根据以上定义,如果在Collections.sort()方法中传入的参数只有一个,那么这个参数必须实现排序接口(implements Comparable)。
以上这段代码中,Integer类也实现了Comparable接口,包括String类,也实现了这个排序接口,所以可以直接传入。
对自定义的对象进行排序有两种方式,
1.让对象对应的类实现Comparable接口并重写compareTo()方法。
2.在sort()方法中传入一个自定义的比较器。
/*
Collections
这是一个用于操作单列集合的工具类
注意跟Collection的区别(Collection是单列集合的根接口)
常用方法
static <T> boolean addAll(单列集合,可变参数) 批量添加元素
static void shuffle(List集合) 打乱List集合元素顺序,每次调用都会打乱
static <T> void sort(List集合) List集合进行自然排序
static <T> void sort(List集合,比较器) List集合进行比较器排序
*/
public class Demo {
public static void main(String[] args) {
//static <T> boolean addAll(单列集合,可变参数) 批量添加元素
List<Integer> list = new ArrayList<>();
Collections.addAll(list,1,3,5,7,9);
System.out.println(list);
//static void shuffle(List集合) 打乱List集合元素顺序,每次调用都会打乱
Collections.shuffle(list);
System.out.println(list);
Collections.shuffle(list);
System.out.println(list);
//static <T> void sort(List集合) List集合进行自然排序
Collections.sort(list);
System.out.println(list);
//排自定义类对象,需要指定排序规则
List<Student> stuList = new ArrayList<>();
stuList.add(new Student("zhangsan", 18));
stuList.add(new Student("wangwu", 22));
stuList.add(new Student("zhaoliu", 21));
stuList.add(new Student("lisi", 19));
stuList.add(new Student("qianqi", 20));
//static<T> void sort(List集合,比较器);List集合进行比较器排序
//对自定义对象进行排序
Collections.sort(stuList, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge()-o2.getAge();
}
});
System.out.println(stuList);
}
}
class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
2.3总结
Collection和Collections的关系:
二者没有关系,
Collection是单列集合根接口,
Collections是一个操作单列集合的工具类。
二、Map集合(双列集合)
1.概述
双列集合根接口
● Map集合称为双列集合,每次需要存一对数据作为一个元素。
格式:{key1 = value1,key2 = value2, key3 = value3,...}
● Map集合的每个元素分为两部分:key和value,key为键,value为值。
整体叫键值对,Map也叫==“键值对集合”==。
● Map集合的所有键是不允许重复的,但是值可以重复,值和键是一一对应的,每一个键只能找到自己对应的值。
2.Map集合使用的业务场景
需要存储一 一对应的数据时,可以考虑用Map集合来实现。
3.Map集合继承体系
Map集合继承体系图谱:
由图可知,没有子接口,直接就是实现类
HashMap底层是哈希表,TreeMap底层是红黑树,LinkedHashMap底层是哈希表+双向链表。
4.Map集合体系特点
Map系列集合的特点由键决定,值只是附属品,不做要求。
● HashMap:无序、不可重复(用的最多)
底层是一张哈希表,本身就没有顺序,根据哈希值取余计算的位置。
是否无序,排序时只看键;对比是否重复时只对比键是否重复,只考虑键。
● LinkedHashMap:有序、不重复
有序是因为底层是一个数组,存的元素虽然无序,但是有一个双向链表,就有顺序了,存取有序。
● TreeMap:按照大小默认升序排列排序、不重复
底层是红黑树,默认升序排列
总结:
5.Map常用方法
6.Map集合遍历方式
7.HashMap
HashMap集合的底层原理
● HashMap和HashSet的底层原理一样,都是基于哈希表实现的
实际上:Set系列集合的底层就是基于Map实现的,只是Set集合中的元素
● 但是它是无序、不能重复(键),没有索引支持的(由键决定特点)
● HashMap的键依赖HashCode()方法和equals()方法保证键的唯一性
● 若键存储的是自定义类型的对象,可以通过重写hashCode()和equals()方法
8.LinkedHashMap
LinkedHashMap集合的底层原理
● 底层数据结构基于哈希表,知识每个键值对元素额外多了一个双向链表的机制记录元素顺序(保证有序)。
实际上:LinkedHashSet集合的底层原理就是LinkedHashMap
9.TreeMap
TreeMap
● 特点:可排序、不重复、无索引
10.补充知识:集合的嵌套
三、Stream流
Stream流是JDK8之后引进的,
JDK8之后引入的两个最大改变:lambda和Stream流,二者都简化了代码的书写
Stream也叫Stream流,是JDK8开始新增的一套API(java.util.stream.*),可用来操作集合或者数组的数据。
四、递归思想
1.认识递归
1.1定义
1.2递归的形式
1.3使用递归需要注意的问题
2.案例计算n的阶乘
day08之IO流->文件&字节流
变量、数组、对象、集合,它们都是内存中的数据容器。
它们记住的数据,在断电或者程序终止时会丢失。
数据若想长久保存起来,需要用到文件这种重要的存储方式,存储在计算机硬盘中。
这样,即使断电或者程序终止,存储在硬盘文件中的数据也不会丢失。
一、File
File是java.io包下的类,File类的对象,用于代表当前操作系统的文件(文件、文件夹)。
File类只能对文件本身进行操作(1.获取文件信息(大小,文件名,修改时间)、2.创建文件/文件夹、3.删除文件/文件夹、4.判断文件的类型),不能读写文件里面存储的数据。
File对象既可以代表文件、也可以代表文件夹。
File对象封装的对象仅仅是一个路径名,此路径可以存在,也可以不存在
也即,不管是文件还是目录,存在与否,都不影响创建对象。
1.创建File类的对象
1.1单参数创建文件
//1. 单参数创建文件
//File(String pathname) 根据指定路径创建File对象
File f1 = new File("E:/upload/test1");//存在-目录
System.out.println(f1);
File f2 = new File("E:/upload/test1/1.txt");//存在-文件
System.out.println(f2);
File f3 = new File("E:/upload/test2");//不存在-目录
System.out.println(f3);
File f4 = new File("E:/upload/test2/2.txt");//不存在-文件
System.out.println(f4);
1.2多参数创建文件
//2. 多参数创建文件
//File(String parent,String child) 根据指定参数拼接的路径创建File对象
File f5 = new File("E:/upload", "test1");
System.out.println(f5);
//File(File parent,String child) 根据指定参数拼接的路径创建File对象
File f6 = new File(f1, "1.txt");
System.out.println(f6);
1.3三种文件分隔符
-
/ 单独使用 /
-
\ 需要转义 \\
-
根据系统自动匹配分隔符,File.separator
//3. 文件分隔符
//一、/可以单独写
File f7 = new File("E:/upload/test1");
System.out.println(f7);
//二、\需要转义
File f8 = new File("E:\\upload\\test1");
System.out.println(f8);
//三、File、separator 根据系统自动匹配分隔符
//兼容性最好
File f9 = new File("D" + File.separator + "upload" + File.separator + "test1");
System.out.println(f9);
1.4绝对路径和相对路径
绝对路径:从盘符开始的路径
相对路径:不带盘符,默认直接到当前工程下的目录寻找文件
//4. 绝对路径 和 相对路径
//绝对路径:从盘符开始的路径
//相对路径:不带盘符,默认直接到<当前工程>下的目录寻找文件。
File f10 = new File("day08/hello2.txt");
f10.createNewFile();
总结:
2.常用方法1:判断文件类型、获取文件信息
getName()方法,如果是目录,获取的就是目录的名字;如果是文件,带着文件的后缀
long lastModified()方法返回的是文件的最后修改时间的毫秒值
/*
常用方法1:判断文件类型、获取文件信息
boolean exists() 判断文件路径是否存在
boolean isFile() 判断是否是文件(不存在的都是false)
boolean isDirectory() 判断是否是文件夹(不存在的都是false)
String getName() 获取文件/文件名,包含后缀
long length() 获取文件大小,返回字节个数
long lastModified() 获取最后修改时间
string getPath() 获取创建对象时的路径
String getAbsolutePath() 获取对象绝对路名
*/
public class Demo2 {
public static void main(String[] args) {
File f1 = new File("day08/test1");//文件夹-已存在
File f2 = new File("day08/test1/1.txt");//文件-已存在
File f3 = new File("day08/test2");//文件夹-不存在
File f4 = new File("day08/test2/2.txt");//文件-不存在
//boolean exists() 判断文件路径是否存在
System.out.println(f1.exists());//true
System.out.println(f3.exists());//true
//boolean isFile() 判断是否是文件(不存在的文件都是false)
System.out.println(f1.isFile());//false
System.out.println(f2.isFile());//true
System.out.println(f3.isFile());//false
System.out.println(f4.isFile());//true
//boolean isDirectory() 判断是否是文件夹
System.out.println(f1.isDirectory());//true
System.out.println(f2.isDirectory());//false
System.out.println(f3.isDirectory());//true
System.out.println(f4.isDirectory());//false
//String getName() 获取文件/文件名,包含后缀
System.out.println(f1.getName());//test1
System.out.println(f2.getName());//1.txt
//long length() 获取文件大小,返回字节个数
System.out.println(f2.length());//5
System.out.println(f4.length());//0
//long lastModified() 获取最后修改时间
long time = f2.lastModified();
System.out.println(time);//1697939990760
System.out.println(new Date(time));//Sun Oct 22 09:59:50 CST 2023
//string getPath() 获取创建对象时的路径
System.out.println(f1.getPath());//day08\test1
System.out.println(f2.getPath());//day08\test1\1.txt
//String getAbsolutePath() 获取对象绝对路径
System.out.println(f1.getAbsolutePath());//E:\workspace\191\javase-advance\day08\test1
System.out.println(f2.getAbsolutePath());//E:\workspace\191\javase-advance\day08\test1\1.txt
}
}
3.常用方法2:创建文件、删除文件
makedirs()方法既可以创建单层目录,也可以创建多层目录。
createNewFile()方法只能创建一个空的文本文件,不能写入内容,File不能写内容,只能创建空文件。
delete方法默认只能删除 文件 和 空文件夹(空目录) ,删除后的文件不会进入回收站。
4.常用方法3:遍历文件夹
4.1File类提供的遍历文件夹的功能
File 类提供的list()方法,获取当前目录下所有一级文件名称,所以调用此方法的File得是一个目录,返回的是一个一级(只有第一层)文件名称的字符串数组(String[ ])。
listFiles()方法获取当前目录下所有一级文件对象,返回的是一个文件对象数组(File[ ])。
4.2使用listFile()方法的注意事项
1.当主调是文件,或者路径不存在时,返回null
(主调:谁在调用方法,谁就是主调)
- 当主调是空文件夹时,返回一个长度为0的数组
3.当主调是一个非空文件夹,但是没有权限访问该文件夹时,返回null。
(无法演示,有的文件夹在系统中设置了访问权限,此时虽然目录中有东西,但依然返回null,因为无法访问)
/*
查看目录中的内容
String[] list() 获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。
File[] listFiles() 获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回,包含隐藏文件(重点)
注意事项
当主调是文件,或者路径不存在时,返回null
当主调是空文件夹时,返回一个长度为0的数组
当主调是一个非空文件夹,但是没有权限访问该文件夹时,返回null
*/
public class Demo4 {
public static void main(String[] args) {
//创建文件(目录)
File file = new File("E:/");
//String[] list() 返回文件名数组
String[] arr = file.list();
//System.out.println(Arrays.toString(arr));
//File[] listFiles() 返回文件数组
File[] files = file.listFiles();
for (File f : files) {
System.out.println(f);
//其实是File对象,但是打印的是文件路径
}
//当主调是文件,或者路径不存在时,返回null
File file1 = new File("E:/1.txt");
String[] list = file1.list();//null
System.out.println(list);
File file2 = new File("E:/haha");
System.out.println(file2.list());//null
//当主调是空文件夹时,返回一个长度为0的数组
File file3 = new File("E:/a");
System.out.println(Arrays.toString(file3.list()));//[]
//当主调是一个非空文件夹,但是没有权限访问该文件夹时,返回null
}
}
4.3总结
5.文件搜索
/*
需求:
从D:盘中,搜索“QQ.exe” 这个文件,找到后直接输出其位置。
分析:
先找出D:盘下的所有一级文件对象
遍历全部一级文件对象,判断是否是文件
如果是文件,判断是否是自己想要的
如果是文件夹,需要继续进入到该文件夹,重复上述过程
*/
public class Demo5 {
public static void main(String[] args) {
//进入文件夹
//D:对文件分类
//文件——>获取文件名——>对比是不是要找的——>如果是,直接返回全路径(绝对路径),结束流程
//文件夹——>重新进入这个文件夹——>重新执行上面的流程
//递归
//此处传参时File的路径名一定要加上/,若不加/,会被当做相对路径
//传的第二个参数要搜索的文件
search(new File("E:/"), "1.txt");
}
/*
在文件夹中搜索一个指定文件名的全路径
*/
/**
* 在文件夹中搜索一个指定文件名的全路径
*
* @param dir 文件夹
* @param name 指定文件名
*/
public static void search(File dir, String name) {
//1.进入文件夹,列出所有文件(文件、文件夹)
File[] files = dir.listFiles();
//只要对集合、数组遍历,都要先做非空判断
//files!=null保证不为空,files.length>0保证不是空数组。
//即既不是null,也不是空数组。
if (files != null && files.length > 0) {
//2.遍历上面的文件,进行判断
for (File file : files) {
if (file.isFile()) {
//3.如果是文件,获取文件名——>对比是不是要找的——>如果是,直接返回全路径(绝对路径),结束流程
String fileName = file.getName();
if (fileName.equals(name)) {
System.out.println("文件全路径:" + file.getAbsolutePath());
return;//结束流程
}
} else {
//4.如果是文件夹,——>重新执行此流程(重新调用本方法)
search(file, name);
}
}
}
}
}
二、字符集
文件最终要落在磁盘上,而磁盘是计算机的一块组成部分,保存所有东西都是以二进制保存的,即0和1。
1.常见字符集介绍
标准ASCII字符集
1.1标准ASCII字符集
● ASCII:美国信息交换标准代码,包括英文、符号等。
● 标准ASCII使用一个字节存储一个字符,首位是0,共可表示128个字符,对于美国人来说完全够用。
ASCII码的首位是0,汉字编码的首位是1。
1.2GBK(汉字内码扩展规范,国标)
在中文简体电脑系统下 ANSI 默认指的是 GB2312
● 汉字编码字符集,包含两万多汉字等字符,GBK中,一个中文字符编码成两个字节的形式存储。
● 注意:GBK兼容ASCII字符集,也即在GBK码表中,0-127号就是ASCII码表。
GBK编码之下英文占一个字节,英文字符首位(最高位,首位是符号位,是0表示是正数,1表示是负数)是0,中文占两个字节。
为了与英文字符区分,GBK规定:汉字的第一个字节的第一位必须是1。
1.3Unicode字符集(统一码,也叫万国码)
● 为了统一编码,Unicode是国际组织制定的,可以容纳世界上所有文字、符号的字符集。
UTF-32:四个字节表示一个字符,不管是中英文,不管是什么语言,想表示一个字符都需要四个字节(因为要容纳所有文字、符号,所以必须要大),用Unicode确实统一了标准,但是产生了极大的空间浪费。
但是这样做占用存储空间,通信效率变低(本来一句话一个字节就能说完,用UTF-32,四个字节才能说完)。
为了解决占用存储空间多,通信效率低的问题,推出了UTF-8。
● UTF-8本质上并不是一种字符集,它是Unicode字符集的一种编码方案,采用可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。
● 英文字符、数字等只占1个字节(兼容标准ASCII编码),汉字字符占用3个字节。
可以看到,“我”在GBK编码表中对应的数字是25105,但是它的UTF-8编码每个字节并不够8位,这是因为UTF-8编码方式,它里面有些字节是内部占用了的。
如果是一个字节,最高位控制好了,就是0,只有剩下7位可以改动,第一个位置不能动。
如果是两个字节,第一个字节里的前三位,第二个字节里的前两位都已固定好不能动,要存只能在剩余位置进行修改,所以会发生上图中编码里一个字节不够八位的情况。
这是UTF-8的内置规则。
汉字字符“我”占用三个字节
1.4总结
GBK和UTF-8的英文、数字编码兼容了ASCII字符集
2.字符集的编码、解码操作
编码:把字符按照指定字符集编码成字节
解码:把字节按照指定字符集解码成字符
三、IO流
IO流是用于读写数据的(可以读写文件或者网络中的数据)
1.IO流概述
输入输出流,负责读写数据。
I:Input,称为输入流,负责把数据读取到程序中来。(站在程序的角度)
O:Output,称为输出流,负责把数据写到存储中去。
2.IO流的应用场景
IO流的分类、体系
IO流的分类
按流的方向分为:输入流和输出流
输入:磁盘——>内存——>程序
按流中数据的最小单位分为:字节流和字符流
字节流适合操作所有类型的文件
如:音频、视频、图片、文本文件的复制、转移等
字符流只适合操作纯文本文件
如:读写txt、Java文件等
IO流总体来看有四大流:
字节输入流InputStream(读字节数据的)
字节输出流OutputStream(写字节出去的)
字符输入流Reader(读字符数据的)
字符输出流Writer(写字符数据出去的)
IO流的继承体系
IO流的作用、用法
IO流的作用:读写文件数据
四、IO流-字节流
文件字节输入流的作用是从磁盘中读取内容到程序中
1.文件字节输入流:每次读取一个字节
每次读取一个字节的方法:public int read()
存在问题:读取性能较差且读取汉字输出会乱码
2.文件字节输入流:每次读取多个字节(非纯文本文件的最佳选择)
每次读取一个字节数组:public int read(byte[ ] buffer)
好处:读取性能得到了提升
3.文件字节输入流:一次读取完全部字节
● 方式一:自定义一个字节数组与被读取的文件大小一样大,然后使用该字节数组,一次性读完文件的全部字节
如果文件过大,创建的字节数组也会过大,可能引起内存溢出。
4.文件字节输出流:写字节出去
5.文件复制
字节流非常适合做一切文件的复制操作
任何文件的底层都是字节,字节流做复制操作,是一字不漏的转移完全部字节。
只要复制后的文件格式一致没问题。