一、继承
1.权限修饰符
用来限制类中成员(成员变量、成员方法、构造器)能够被访问的范围。
private:只能在本类中被访问
缺省:在本类、一个包中的所有类访问
protected:在本类、同一个包中的类、子孙类中访问
public:任意位置访问
2.继承的特点
1.单继承:一个类只能继承一个直接父类
2.多层继承:Java不支持多继承,但支持多层继承
3.祖宗类:Java中所有类都是Object类的子类。一个类要么直接继承Object,要么默认继承Object
4.就近原则:优先访问自己类中,自己类中没有再去访问父类,若父类也没有就会报错;如果子类和父类中出现了重名的成员,会优先使用子类的,若此时一定要访问父类的成员,则可通过super关键字来指定访问父类的成员
3.方法重写
当子类觉得父类中的某个方法不好用,或者无法满足自己的需求时,子类可以重写一个方法名称和参数列表一样的方法,去覆盖父类的这个方法。
方法重写的注意事项:使用方法重写时,建议在方法上注明@Override,即方法重写的校验注解(标记注解),从而告诉别人这是方法重写,若不加@Override,当名称或形参与被重写方法不一致时,没有报错提醒,会误以为已经方法重写了。
其他注意事项:
(1)子类重写父类方法时,访问权限必须大于等于父类该方法的权限(public>protect>缺省>private)
(2)重写的方法返回类型,必须与重写方法的返回值类型一致,或者范围更小9
(3)私有方法、静态方法不能被重写,如果重写会报错,即子类只能继承父类的非私有方法
应用场景:子类可以重写Object类的toString()方法,以便返回对象的内容
4.子类构造器的特点
子类的全部构造器,必须会优先调用父类的构造器,再执行自己的构造器。

若要调用父类的有参构造器,则可使用super关键字来调用。

子类构造器是如何实现调用父类构造器的:
- 默认情况下,子类全部构造器的第一行代码都是super(),它会调用父类的无参数构造器
- 如果父类没有无参数构造器,则必须在子类构造器的第一行手写super(参数),指定去调用父类的有参数构造器
调用兄弟构造器时使用this(),即this()是用来在构造器中调用本类的其他构造器;
this()和super()是构造器之间相互调用使用的语法,而this和super是指父类和子类的对象。
(1)对this()和super()来说:
每个构造器默认的第一行是this()和super()的其中之一,super()是用来调用父类构造器,而this()是用来调用同类中的兄弟构造器,因此this()和super()不能同时出现,且只能存在第一行。有时候父类没有无参数构造器或者子类需要调用父类的有参数构造器,那么在子类中的第一行就得声明要调用父类的有参数构造器。
(2)对this和super来说:
this和super都不能出现在静态方法和静态块中,因为静态方法是类方法,只能在本类中使用
二、多态
多态是在继承/实现情况下的一种现象,表现为:对象多态、行为多态。比如对象是一个人,人又可以分为男、女,即对象是多态的;行为多态,如学生跑步跑得慢,老师跑步跑得快。
java
People p1 = new Student();
p1.run();
People p2 = new Student();
p2.run();
对行为(方法)来说:编译看左边,运行看右边。如下所示代码,在执行a1.run()时,编译时先看Animal中有没有run(),然后执行时,再看Wolf()中的run()方法。
对成员变量来说:编译看左边,运行也看左边,即成员变量都为Animal中的成员变量。

多态的前提:
(1)有继承/实现关系;
(2)存在父类引用子类对象:即父类变量指向子类对象,即People p1 = new Student();
java
People p1 = new Student();
p1.run();
People p2 = new Teacher();
p2.run();
(3)存在方法重写
多态是对象和行为的多态,Java中的属性(成员变量)不谈多态
多态的优点:
(1)在多态形式下,右边对象是解耦合的(即可随时拆卸),更便于扩展和维护;假设目前有如下代码:
java
Animal a1 = new Wolf();
a1.run();
此时,若我们嫌狼跑的太快了,那就可以直接换为乌龟:
java
Animal a1 = new Tortoise();
a1.run();
(2)定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
假设现在设计了一款电子宠物赛跑的游戏,其中有一段代码为:
java
Wolf w = new Wolf(); //假设此时Wolf类是Animal的子类
go(w);
java
public static void go(Wolf w){
System.out.println("预备!");
w.run();
}
那么此时,go()这个方法就只能接受w这个对象,而不能接收其他对象,假设还存在其他对象:
java
Tortoise t = new Tortoise; //假设此时Tortoise类是Animal的子类
若想给t也赋予一个跑步的行为就需要再写一个go方法。因此为了简单起见,此时就可将Wolf w更换为Animal a
java
public static void go(Animal a){
System.out.println("预备!");
a.run();
}
此时,既可以接Wolf 又可以接 Tortoise,扩展性更强、更便利。
但多态下会产生一个问题:多态下不能使用子类的独有功能。如下图所示,红线标注的为动物(子类)特有功能。因为乌龟缩头是乌龟特有的功能,没有出现在Animal类中,故不能调用因此就不能使用a.shrinkHead()来直接调用。

如何解决这个问题呢?这就涉及到多态下的类型转换问题了:
(1)自动类型转换:父类 变量名 = new 子类();
(2)强制类型转换:子类 变量名 = (子类)父类变量

这就完成了类型转换,但是在使用强制类型转换时需要注意以下两点:
(1)存在继承/实现关系就可以在编译阶段进行强制类型转换,编译阶段不会报错;
(2)运行时,若发现对象的真实类型与强转后的类型不同,就会保类型转换异常(ClassCastException)的错误
java
People p = new Teacher(); //p指向Teacher
Student s = (Student) p; //用Student接,会报错
有继承关系就可以强转,但因为第一行转换的是Teacher类型,而第二行却用Student类型来接,虽然在编译阶段不报错,但实际运行时会报错。因此,在强转前可以使用instanceof关键字来判断当前对象的真实类型,再进行强转。
java
p instanceof Student //判断p是否是Student类型,若是则返回真,否则返回假,一般用在if中
三、面向对象编程高级
1.final关键字
(1)认识final关键字
final关键字是最终的意思,可以修饰:类、方法和变量。
- 修饰类:该类被称为最终类,特点是不能够被继承;一般用在工具类中,因为工具类一般不会被继承
- 修饰方法:该方法被称为最终方法,特点是不能被重写;
- 修饰变量:java中变量包括成员变量 和局部变量 ,该变量有且仅能被赋值一次 ,且使用final修饰变量时一定要赋值。
成员变量包括:静态成员变量 和实例成员变量
final修饰静态成员变量时,该变量今后被称为常量,可以记住一个固定值,并且在程序中不能被修改,通常该值作为系统的配置信息;常量的名称建议全部大写,每个单词用下划线隔开。
java
public static final String MY_NAME = "大佬";
final修饰实例变量一般没有意义,且已知实例变量属于对象,此时若用final修饰实例变量,就会导致后续创建的所有对象的实例变量都不能再次被修改。
java
private final String name = "大佬"; // 后续创建的所有对象的name都为"大佬"
使用final修饰变量时要注意:
- final修饰基本类型的变量,变量存储的数据不能被改变
java
final int a = 20; //a中存储的20不能被改变
- final修饰引用类型的变量(如数组、对象),变量存储的地址不能被改变,但地址所指向对象的内容是可以被改变的
(2)常量
使用了static final修饰的成员变量就是常量,常量被用于记录系统的配置信息,具体应用场景为:若此时想要开发一个智能家居系统,名称为"华为全屋智能系统",在整个项目中,这个名称肯定会在多个地方被使用,而某天我们突然想改名为"华为AI+全屋智能系统"就不得不在所有代码中一个一个去修改名称,这是非常麻烦的,因此我们可以创建一个常量来存储这个名称,当我们需要改名时,只需在常量处修改即可。一般在企业中定义常量会单独创建一个Constant包,在这个包中会定义系统中需要的各种常量。
优点除了代码可读性更好和维护性更好外,在程序编译后,常量会被"宏替换":出现常量的地方全部被替换为其记住的字面量,这样可以保证使用常量和直接使用字面量的性能是一样的。
2.单例类(设计模式)
单例类设计模式的作用是**确保某个类只能创建一个对象。**有这样一个场景:任务管理器,当打开任务管理器时不管你开多少个他始终都只显示一个,那么对于任务管理器来说,就需要确保其只能被创建一个。
(1)饿汉式单例类
单例类的写法及实现步骤:
(1)把类的构造器私有:构造器的私有能够保证在外部不能任意创建对象
(2)定义一个类变量来记住本类的一个唯一对象,此时需用private修饰,避免唯一对象的丢失。如在外部使用A.a = null。
(3)定义一个类方法,返回对象 。由于上一步使用了private修饰,此时需要写一个方法返回对象,供外部使用。
java
//单例类
public class A{
//2.定义一个类变量记住类的一个对象
private static A a = new A();
//或 public static final A a = new A();
//1.私有构造器
private A(){
}
//3.定义一个类方法返回对象
public static A getObject(){
return a;
}
}
小结:上述的这种写法称为饿汉式单例类:用对象时,对象已经创建好了

(2)懒汉式单例类
用对象时,才开始创建对象
(1)把类的构造器私有:构造器的私有能够保证在外部不能任意创建对象
(2)定义一个类(静态)变量来记住本类的一个唯一对象,此时需用private修饰,避免唯一对象的丢失。如在外部使用B.a = null。
(3)定义一个类(静态)方法,返回对象 。由于上一步使用了private修饰,此时需要写一个方法返回对象,供外部使用。
java
//单例类
public class B{
//2.定义一个类变量记住类的一个对象
private static B b; // null
//1.私有构造器
private B(){
}
//3.定义一个类方法返回对象
public static B getObject(){
if(b == null){
b = new B();
}
return a;
}
}
3.枚举类
(1)枚举类是一种特殊类
(2)枚举类的写法
java
修饰符 enum 枚举类名{
名称1, 名称2,...;
其他成员...
}
java
public enum A{
X, Y, Z;
...
}
特点:
- 枚举类中的第一行,只能写枚举类的对象名称,且要用逗号隔开。
- 这些名称,本质是常量 ,每个常量都记住了枚举类的一个对象。

java
System.out.println(a.name()); //打印名称
System.out.println(a.ordinal()); //打印索引,若定义的对象为X,则返回0,若是Y,则返回1
枚举类的常见应用场景
- 枚举类很适合做信息分类和标志,通知底层需要做什么操作

4.抽象类
abstract关键字:抽象类关键字,他可以修饰类,也可以修饰成员和方法。
abstract修饰类时,该类即为抽象类;abstract修饰方法时,该方法即为抽象方法;
java
修饰符 abstract class 类名{
修饰符 abstract 返回值类型 方法名称(形参列表);
}
java
public abstract class A{
//抽象方法:必须abstract修饰,只有方法签名(只包含方法的名称及返回类型),不能有方法体
public abstract void test();
}
注意事项
- 抽象类中不一定有抽象方法,但有抽象方法的类必须是抽象类;
- 类中有的,如成员变量、方法、构造器等,抽象类都可以有;
- **抽象类最重要的特点:**抽象类不能够创建对象,仅作为一种特殊的父类让子类继承并实现;
- 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则该类也必须定义为抽象类。


当然,抽象类也不是必须的,即便是父类不使用抽象类也是可以的。
模板方法设计模式
- 提供一个方法作为完成某类功能的模板,模板方法封装了每个实现步骤,但允许子类提供特定步骤的实现。
- 模板方法设计模式可以:提高代码的复用、并简化子类设计。

写法 :1.定义一个抽象类;2.在里面定义2个方法:①一个是模板方法:把共同的实现步骤放进去;②一个是抽象方法:不确定的实现步骤,交给具体的子类去完成。


**建议:**使用final关键字来修饰模板方法,这是因为使用final来修饰模板方法可以避免模板方法被重写而导致模板被破坏

5.接口
- java提供了一个关键字Interface定义出接口
java
//这种定义方式为JDK8之前的传统定义方式
//其只能包含常量及抽象方法
public interface 接口名{
//成员变量(常量) 定义时默认为常量,必须赋值,前面的public static final可以省略
//成员方法(抽象方法) 定义时默认为抽象方法,不能写方法体,前面的public abstract可以省略
}
注:接口不能创建对象
接口是用来被类实现(implements)的,实现接口的类被称为实现类,一个类可以同时实现多个接口。(而父类只能有一个,因此接口相当于干爹,可以有多个 )
java
修饰符 class 实现类类名 implements 接口1, 接口2, 接口3...{
//实现类实现多个接口,必须重写完全部接口的全部抽象方法(Alt + 回车 + 回车),否则实现类需要定义成抽象类
}
接口的好处

JDK8之后新增的3个方法
- 默认方法(普通实例方法),必须加default修饰,该方法默认会加public修饰,可以使用接口的实现类的对象来调用;
- 私有方法,无法通过接口外的方法调用,只能使用接口中其他实例方法来调用;
- 静态方法 ,默认public修饰,只能使用当前接口名来调用(父类中的静态方法可以通过子类来调用)

接口的注意事项
1、接口与接口可以多继承:一个接口可以同时继承多个接口重点。

2、一个接口继承多个接口,如果多个接口中存在方法签名冲突,则此时不支持多继承,也不支持多实现。

3、一个类继承了父类,又同时实现了接口,如果父类中和接口中有同名的默认方法,实现类会优先用父类的。

4.一个类实现了多个接口,如果多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。

抽象类与接口的区别:
相同点:
1、都是抽象形式,都可以有抽象方法,都不能创建对象。
2、都是派生子类形式:抽象类是被子类继承使用,接口是被实现类实现。
3、一个类继承抽象类,或者实现接口,都必须重写完他们的抽象方法,否则自己要成为抽象类或者报错!
4、都能支持的多态,都能够实现解耦合。
不同点:
1、抽象类中可以定义类的全部普通成员,接口只能定义常量,抽象方法(JDK8新增的三种方式)
2、抽象类只能被类单继承,接口可以被类多实现。
3、一个类继承抽象类就不能再继承其他类,一个类实现了接口(还可以继承其他类或者实现其他接口)。
4、抽象类体现模板思想:更利于做父类,实现代码的复用性。最佳实践
5、接口更适合做功能的解耦合:解耦合性更强更灵活。最佳实践
6.代码块
代码块是类的五大成分之一:成员变量、构造器、方法、代码块及内部类;按照有无static修饰分为:静态代码块 和实例代码块
- 静态代码块:
- 格式:
static{} - 特点:类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次
- 作用:完成类的初始化,例如:对静态变量的初始化赋值,且只初始化一次
- 格式:

- 实例代码块:
- 格式:
{} - 特点:每次创建对象时,执行实例代码块,并在构造器前执行
- 作用:和构造器一样,都是用来完成对象的初始化的,例如:对实例变量进行初始化赋值
- 格式:

7.内部类
-
若一个类定义在另一个类的内部,则这个类称为内部类
-
场景:当一个类的内部,包含了一个完整的事物,且这个事物没有必要单独设计时,就可以将这个事物设计成内部类

(1)成员内部类
- 就是类中的一个普通成员,类似于成员变量、成员方法
java
public class OuterP{
//成员内部类
public class Inter{
}
}




(2)静态内部类


(3)匿名内部类(重点)
- 匿名内部类是一种特殊的局部内部类
- 所谓匿名指的是程序员不需要为这个类声明名字,默认有一个隐藏的名字
java
new 类或接口(参数值){ //已有的类或接口
类体(一般是方法重写);
}
java
new Animal(){
@Override
public void cry(){
}
}
- 特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
- 作用:用于更方便的创建一个子类对象。


- 匿名内部类在开发中的常见形式

-
匿名内部类在开发中的真实使用场景示例
- 调用别人提供的方法实现需求时,这个方法正好可以让我们传输一个匿名内部类对象给其使用。
java‘JButton btn = new JButton("登录"); //给按钮绑定单击事件监听器对象,可以用来监听用户的点击,以便做出反应 btn.addActionListener(new ActionListerner(){ @Override public void actionPerformed(ActionEvent e){ JOptionPane.showMessageDialog(win, "没事别点我!讨厌!"); } )};
- 使用comparator接口的匿名内部类实现对数组进行排序
java
public calss Test{
public static void main(String[] args){
Student[] students = new Student[3];
students[0] = new Student("玛卡巴卡", 16, 170.1);
students[1] = new Student("唔西迪西", 12, 158.2);
students[2] = new Student("依古比古", 18, 168.5);
Arrays.sort(student, new Comparator<Student>(){
@Override
public int compare(Student o1, Student o2){
return o1.getAge() - o2.getAge();
}
});
System.out.println(Arrays.toString(students));
}
}

8.函数式编程
-
Lambda表达式
- JDK8开始新增的一种语法形式,它表示函数
- 可以用于替代某些匿名内部类对象,从而让程序更简洁,可读性更好
java(被重写方法的形参列表)-> { 被重写方法的方法体代码。 }注:Lambda表达式只能替代函数式接口的匿名内部类!!!
什么是函数式接口?
- 有且仅有一个抽象方法的接口



(1)方法引用
-
静态方法引用
- 类名::静态方法
- 使用场景:若某个Lambda表达式中只是调用一个静态方法,并且"→"前后参数的形式一致,就可以使用静态方法引用

(2)实例方法引用
-
对象名::实例方法
-
使用场景
- 若某个Lambda表达式里只是通过对象名称调用一个实例方法,并且"→"前后参数的形式一致,就可以使用实例方法引用。

(3)特定类型的方法引用
- 特定类的名称::方法
- 使用场景
- 若某个Lambda表达式里只是调用一个特定类型的实例方法,且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参的,则此时就可以使用特定类型的方法引用

(4)构造器引用
- 类名::new
- 使用场景
- 若某个Lambda表达式里只是在创建对象,并且"→"前后参数情况一致,就可以使用构造器引用
9.常用API
(1)String
-
String代表字符串,它的对象可以封装字符串数据,并提供了很多方法完成对字符串的处理
-
String创建字符串对象的方式
- 方式一:Java中所有字符串文字(例如"abc")都为此类的对象
javaString name = "小黑"; String schoolName = "黑马程序员";- 方式二:调用String类的构造器初始化字符串对象

-
示例如下:

上述这两种String创建对象的区别:
- 只要是以**"..."方式写出的字符串对象,会存储到 字符串常量池**,且相同内容的字符串只存储一份;
- 通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中

- 两种创建方式的底层对比:


- String提供的常用方法




- String生成验证码

(3)ArrayList
- 集合是一种容器,用来存储数据,类似于数组。创建ArrayList对象,就代表一个集合容器,调用ArrayList提供的方法,对容器中的数据进行增删改查操作。
- 数组的缺点:定义完成后其长度就固定不变了。
- 集合的大小可变




10.异常
- 异常代表程序出现的问题

(1)Java的异常体系

- 运行时异常

- 编译时异常

(2)解决方法

(3)异常的作用
- 作用一
异常是用来定位程序bug的关键信息
- 作用二
可以作为方法内部的一种特殊返回值,以便通知上层调用者,方法的执行问题

上图所示的方法就是采用运行时异常 ,将异常的信息向上层调用者传递,告诉其输入参数有误,当然也可以用下图的编译时异常的方式传递,这样的方式提醒更加强烈

(4)小结
1.异常是什么?
异常是代码在编译或者执行的过程中可能出现的错误。
2.异常的代表是谁?分为几类?
Exception,分为两类:编译时异常、运行时异常。
编译时异常:没有继承RuntimeExcpetion的异常,编译阶段就会出错。
运行时异常:继承自RuntimeException的异常或其子类,编译阶段不报错,运行时出现的。
3.异常的作用是啥?
用来查找bug;可以作为方法内部的特殊返回值,通知上层调用者底层的执行情况。
(3)自定义异常
Java无法为这个世界上所有的问题都提供异常类来代表,若企业自己的某种问题,想通过异常来表示,以便用异常来管理该问题,那就需要自己来定义异常类。

-
自定义编译时异常

-
自定义运行时异常(用的最多)
运行时异常在上层中可以不处理,但是也可以用try/catch处理,使用Ctrl+Alt+T即可调出

(4)异常的处理方案
-
方案一(最常用)
底层异常往往向上抛出,最外层捕获异常,记录下异常信息,并响应适合用户观看的信息进行提示,如在b站、新浪等网页搜索不存在的网址时,他就会弹出不存在的界面来提醒用户搜索异常。

- 方案二
最外层捕获异常后,尝试重新修复。

11.泛型
(1)定义
- 定义类、接口、方法时,**同时声明了一个或者多个类型变量(如:
<E>)**称为泛型类、泛型接口、泛型方法,他们统称为泛型。
java
public class ArrayList<E>{
...
}
- 作用:泛型提供了在编译阶段约束所能操作的数组类型,并自动进行检查的能力,这样可以避免强制类型转换,及可能出现的异常。
- 泛型的本质 :把具体的数据类型作为参数传给类型变量。如将
<String>传给<E>那么后续的操作中E就代表String

(2)泛型类


(3)泛型接口


(4)泛型方法



(5)通配符
就是"?",可以在**"使用泛型"的时候代表一切类型; ETKV是在定义泛型的时候使用**。

但是通配符也存在一个很大的问题,假如此时又创建了一个dog对象,它也要调用go这个方法,那么狗和车赛跑显然是不合理的,但通配符并不会解决这个问题。

那么为了解决这个问题,java又诞生了一个新语法:
(6)泛型的上下限
- 泛型上限:
?extends Car:?能接收的必须是Car或者其子类。 - 泛型下限:
?super Car:?能接收的必须是Car或者其父类。

(7)泛型支持的数据类型
- 泛型不支持基本数据类型,只能支持对象类型(引用数据类型)。

究其原因,主流观点认为与泛型擦除有关,所谓泛型擦除就是:泛型工作在编译阶段,等编译后泛型就没用了,所以泛型在编译后都会被擦除,所有类型都会恢复成Object类型。即所有的E都恢复成Object。那么若输入的是个整型,就相当于是用Object去接一个整型,这是不合理的的,Object应该指向的是对象。
那为了解决这个问题就有诞生了包装类这个概念,实现万物皆对象这个思想,其基本思想就是,将基本类型的数据也变成对象数据,相当于一个整型数据,将他放到对象中供其使用。
(8)包装类
- 包装类就是把基本数据类型的数据包装成对象的类型。

- 手工包装

- 自动装箱

- 自动拆箱

同理的,我们从中取出包装类对象,也可以用int来接,因为它会自动拆箱,将包装类拆为基本数据类型。
-
包装类具备的其他功能


(9)小结

12.集合
(1)概念
- 集合是一种容器,用来存储数据,类似于数组,但集合的大小可变,开发中最常用
(2)集合体系结构
-
Collection :代表单列集合,每个元素(数据)只包含一个值。
-
Map:代表双列集合,每个元素包含两个值(键值对)

(3)Collection


- Collection的常用功能
Collection是单列集合的祖宗,它规定的方法是全部单列集合都会继承的。


- Collection的三种遍历方式
(1)迭代器遍历
是用来遍历集合专用的 方式(数组没有迭代器),在java中迭代器的代表是Iterator

java
package diedaiqi;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Test {
public static void main(String[] args){
//掌握Collection的遍历方式一:迭代器遍历
Collection<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
names.add("赵六");
//得到集合的迭代器对象
Iterator<String> it = names.iterator();
// System.out.println(it.next()); //获取当前位置元素,同时将迭代器对象指向下一个元素位置
// System.out.println(it.next());
// System.out.println(it.next());
//使用while循环遍历
while(it.hasNext()){
String name = it.next();
System.out.println(name);
}
}
}

(2)增强for循环
java
for(元素的数据类型 变量名 : 数组或者集合){
}
java
Collection<String> c = new ArrayList<>();
for(String s : c){
System.out.println(s);
}
- 增强
for循环可以用来遍历数组 或者集合 - 增强
for遍历集合,本质就是迭代器遍历集合的简化写法
java
String[] users = {"张三", "李四", "王五", "赵六"};
for(String user : users){
System.out.println(user);
}
(3)Lambda表达式


(4)三种方式的区别
在了解这三种遍历方式的区别前,需要引入一个问题:并发修改异常问题。即在遍历集合的同时又存在增删集合元素的行为时可能出现的异常。

在存在索引的集合中,可以利用for循环来遍历删除,但要注意,在正向删除时,就会引发漏删的问题,原因是删除一个元素后,后续的元素会向前补位,但此时的i又会指向下一个位置,从而漏删。解决方法也很简单,就是在删除后让i--即可,或是从后向前遍历。
那么对于无索引的集合中,就只能使用前面学的三种遍历方式来删除了:

因此可以看出,迭代器可以用作遍历和删除同时进行的操作,而增强for和Lambda只适合用作遍历。
- 小结

13.List集合的特有方法
List集合因为支持索引,所以多了很多索引相关的方法,当然,Collection的功能List也都继承了

(1)ArrayList和LinkedList的区别
ArrayList底层是基于数组实现的

-
数组的底层原理
- 数组刚被创建时其长度为
0,是在每次添加第一个元素时才被扩充为默认长度10,当该数组被填满后,又会进行二次扩容,扩容到原来的1.5倍,即15。
- 数组刚被创建时其长度为
-
LinkedListd底层是基于双链表存储数据的

LinkedList应用场景:①用于设计队列②用于设计栈
14.集合框架
(1)Set集合

- HashSet集合的底层原理


但当数据足够多时,所链接的数据就会无限长,这会很大影响哈希表的性能,因此Java做了一个扩容机制:
- 当哈希表中所存储的数据长度已经达到:
16×0.75=12时,哈希表就会开始扩容,扩容后的长度为之前长度的2倍,即32,然后再将后续的数据加载到扩容后的哈希表中。
但这种方式仍然存在一个问题:虽然进行了扩容,此时很巧存入的值都为一个位置,即链表越来越长,仍然会降低哈希表的性能,因此java在JDK8之后增加了一个新的机制:
- 当链表长度超过
8时,且此时哈希表扩充长度≥64,Java会自动将链表转变为红黑树,从而提高检索性能。

- 小结

- HashSet的去重操作
在创建对象并进行存储时,用HashSet并不能完成去重操作,原因是每new一个对象,每个对象的地址都是不同的(即便创建对象中的数据都是相同的),那么每个对象对于的哈希值也是不同的,因此在存储时,哈希表会将他们当作不同的数据去存储。
因此,若我们希望HashSet认为两个内容相同的对象是重复的,就需要重写HashCode()和equals()方法

(2)Map



- 小结

(1)Map集合的常用方法


注意:放值的时候我们就不需要它不重复了,如果不重复就会导致某些值的丢失,因此就需要将值放入Collection集合中。
(2)Map集合的遍历方式
- 方式一:键找值


- 键值对遍历
Java在利用键值对进行遍历时发现,在使用增强for循环时,遍历元素的类型不能被确定下来(键值分别为不同的数据类型),因此它就提供了一个方法:

利用entrySet()方法可以将键值对转变为一个Set集合,也就是将一个键值对打包成一个整体放到Set集合中,再去遍历:


- Lambda遍历方式


实际上:原来学的Set系列集合的底层就是基于Map实现的,只是Set集合中的元素只要键数据,不要值数据而已。
(3)Stream流
- 简介
Stream流是Jdk8开始新增的一套APl(java.util.stream.*),可以用于操作集合或者数组的数据。

- Stream流的使用步骤

- 小结

(1)获取数据的Stream流


(2)Stream流的常用方法

若想对对象进行去重,则需要重写equals()和hashCode()方法才能正常进行去重

(3)Stream流的终结方法


- 收集Stream流
在开发中,不会使用流去接数据的,接数据实际上还是数组或是集合。也就是说,用Stream流处理完数据后要收集到数组或是集合中。

(4)方法中的可变参数
- 就是一种特殊形参,定义在方法、构造器的形参列表中,格式为:数据类型...参数名称;
- 特点:可以不传数据给它,也可以传一个或者同时传多个数据给它;也可以传一个数组给它
- 优点:常用来灵活的接受数据。

- Collections工具类


15.File-IO流(用到再学)
(1)存储数据的方案
File是java.io.包下的类,File类的对象用于代表当前操作系统的文件(可以是文件、或文件夹)File类只能对文件本身进行操作(如获取文件信息、创建or删除文件),不能读写文件中存储的数据- 用于读写数据的为**
IO流**,可以读写文件或网络中的数据等。如我们打游戏时获得的一个最高分,可通过IO流写入磁盘中,防止掉电丢失,下次想看最高分时又可将其读回游戏的程序中
(2)File文件
File类的对象可以代表文件或文件夹,并可以调用其提供的方法对象文件进行操作。

-
注意事项:
File对象既可以代表文件,也可以代表文件夹;File封装的对象仅仅是一个路径名,这个路径可以是存在的,也可以不存在。
-
File提供的创建和删除文件夹的方法

Flie提供的遍历文件夹的方法


16.多线程
(1)概述
- 线程
(Thread)是一个程序内部的一条执行流程
java
public static void main(String[] agrs){
//代码...
}
- 程序中若只有一条执行流程,那这个程序就是单线程的程序
(2)多线程的创建
(1)方式一:继承Thread类


执行过程中,主线程和子线程的执行顺序是不一样的,也就是交替执行的。且要注意的是:线程类已经继承了Thread,无法继承其他类。
-
其他注意事项:
- 启动线程必须调用
start方法而不是run方法,直接调用run方法会当作普通方法执行
- 启动线程必须调用

- 不要把主线程任务放在子线程之前跑,这样会先跑完主线程才会执行子线程,就成单线程了,体现不出多线程并发性
(2)方式二:实现Runnable接口

- 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
- 缺点:需要多一个
Runnable对象。
方法二还可以用匿名内部类来写:


(3)方式三:实现Callable接口
前两种创建方式都存在一个问题:重写的run方法均不能直接返回结果 ,JDK5.0提供了Callable接口和FutureTask类来实现,,这种方式创建的线程可以返回执行后的结果




(3)Thread的常用方法

子线程的默认名称为Thread-索引,如Thread-0,Thread-1等,主线程的名称为main,若想获取主线程的线程名称,即可在主线程中调用并返回public static Thread currentThread()即可拿到主线程(谁调用这段代码就会拿到哪个线程),然后再返回名称即可,但需注意,setName需要放在start之前:
java
Thread m = Thread.currentThread();//m调用该代码,就获得m
System.out.println(m.getName());//返回m线程的名称


(4)线程安全
- 概述
当多个线程同时操作同一个共享资源时,可能会出现业务安全问题,出现问题的原因有:
- 存在多个线程同时进行
- 同时访问一个共享资源
- 存在修改该共享资源
模拟线程安全问题


(5)线程同步
-
基于上述问题,就给出了一个解决方案,即线程同步,通过加锁来确保线程安全
-
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
线程同步有三种方式控制加锁:(1)同步代码块(2)同步方法(3)lock锁
(1)方式一:同步代码块
- 作用:把访问共享资源的核心代码上锁,以此保证线程安全
java
synchronized(同步锁){
访问共享资源的核心代码
}
- 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
同步锁的注意事项:
- 对于当前同时执行的线程来说,同步锁必须是同一把锁(即同一个对象),否则会产生bug
- 要注意,这个唯一的锁对象并不能随便选择,以下面代码为例,当我们随便输入一个锁对象时,对于Tom和Jerry是一组,而小黑和小白是另一组,那么当线程Tom进行取钱的操作时,剩下所有人都不能再进行取钱操作,这是矛盾的,因为Tom和Jerry取钱和小黑、小白并没有关系,他们不是同一个账户,相当于你去银行取钱,此时全世界的人都得停下来等你取完才能让下一个人取,这显然是不合理的

- 因此,需要规定锁对象的使用规范
- 建议使用共享资源作为锁对象,对于上面的代码来说,两人的账户就是共享资源;对于实力方法建议使用this作为锁对象
- 对于静态方法建议使用**字节码(类名.class)**对象作为锁对象
(2)方式二:同步方法
- 作用:把访问共享资源的核心方法上锁,以此保证线程安全
java
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源的代码
}
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行

- 同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
- 若方法是实例方法:同步方法默认用
this作为锁对象 - 若方法是静态方法,同步方法默认用类名
class作为锁对象

(3)方式三:lock锁
- lock是接口,不能直接实例化,可以采用它的实现类
ReentrantLock来构建Lock锁对象

- Lock的常用方法



17.线程池
(1)了解线程池
- 线程池就是一个可以复用线程的技术(重复利用)
- 若不使用线程池将会有以下问题:
- 用户没发起一个请求,后台就会创建一个新线程来处理,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程,这样会严重影响系统性能。
线程池内部有负责处理任务的工作线程(WorkThread)及任务队列(WorkQueue),每当一个任务来临,就会加载到任务队列中,工作线程会挨个处理,每当一个线程处理完一个任务后,就会继续去处理队列中其他的任务。
- 注:任务队列中只能放
Runnable和Callable
(2)创建线程池
- 线程池接口:
ExecutorService - 方式一:使用
ExecutorService的实现类ThreadPoolExecutor自建一个线程池对象

- 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
(1)通过ThreadPoolExecutor创建线程池


- 这七个参数较为重要,需要记住
(2)ExecutorService的常用方法

- 执行Runnable任务

- 执行Callable任务

(3)线程池的注意事项
- 临时线程的创建时机
- 四个条件:①有定义临时线程的名额②核心线程都在工作③任务队列已满④有新任务尝试加入任务队列
- 拒绝新任务的时机
- 三个条件:①核心线程和临时线程都在忙②任务队列已满③有新的任务尝试加入任务队列


(4)通过Executors创建线程池
- 是一个线程池的工具类,提供若干静态方法用于返回不同特点的线程池对象

newFixThreadPoll()源码:

- 由此可以看出,这些方法的底层都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象
(5)Executors工具类可能存在的风险

- 建议还是使用
ThreadPoolExecutor手动定义7个参数
(5)核心线程数量的选择
java中,线程池的核心线程数量(corePoolSize)和最大线程数量(maxmumPoolSize)的配置通常由应用程序的特性来决定,如:是CPU密集型 还是IO密集型
- CPU密集型任务(计算密集型)线程池的大小通常设置为系统可用处理器数量相等或稍多一点,以避免过多的上下文切换和减少线程调度的开销。
java
int corePoolSize = Runtime.getRuntime().availableProcessor();
int maximumPoolSize = corePoolSize + 1;//或者等于corePoolSize
- IO密集型任务(如网络请求,磁盘读写等),线程可能大部分时间都在等待IO操作完成,此时可以设置更多的线程来充分利用CPU等待时间。
java
double blockingFactor = 0.9;//阻塞系数,例如90%的时间在等待I0
int corePoolSize=(int)Math.ceil(Runtime.getRuntime().availableProcessors() /(1 - blockingFactor);
int maximumPoolSize = corePooLSize * 2;//或者更高,根据具体需求调整
(6)并发和并行
- 进程:正在运行的程序(软件)就是一个独立的进程
- 线程是属于进程的,一个进程中可以同时运行很多个线程, 进程中的多个线程其实是并发 和并行执行的。
- 并发的含义:进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行, CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
- 并行 的理解:在同一个时刻上,同时有多个线程在被CPU调度执行。并发和并行同时进行
18.网络编程
(1)基本通信架构
基本的通信架构有2种形式:CS架构(Client客户端/Server服务端)、BS架构(Browser浏览器/Server服务端)。


(2)网络通信三要素

(1)IP地址


(1)IP域名(Domain name)
用于在互联网上识别和定位网站的人类可读的名称。 例如: www.baidu.com www.itheima.com
-
DNS域名解析 (Domain Name System)
是互联网中用于将域名转换为对应IP地址的分布式命名系统。它充当了互联网的"电话簿",将易记的域名映射到数字化的IP地址,使得用户可以通过域名来访问网站和其他网络资源。
-
公网IP和内网IP
- 公网IP:是可以连接到互联网的IP地址;
- 内网IP:也叫局域网IP,是只能组织机构内部使用的IP地址;例如,192.168.开头的就是常见的局域网地址,范围为 192.168.0.0 ~192.168.255.255,专门为组织机构内部使用。
- 本机IP
127.0.0.1、
localhost:代表本机IP,只会寻找当前程序所在的主机。 - IP常用命令
ipconfig:查看本机IP地址。ping IP地址:检查网络是否连通
(2)InetAdress常用方法
InetAdress代表Ip地址


- 小结

(2)端口
用来标记标记正在计算机设备上运行的应用程序,被规定为一个16位的二进制,范围是0~65535。
- 端口分类
- 周知端口:
0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21) - 注册端口:
1024~49151,分配给用户进程或某些应用程序。 - 动态端口:
49152~65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。 - 注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则报错。
- 周知端口:
- 小结
- 端口号的作用是什么?
- 唯一标识正在计算机设备上运行的进程(程序)
- 一个设备中,能否出现
2个应用程序的端口号一样,为什么?- 不可以,如果一样会出现端口冲突错误
- 端口号的作用是什么?
(3)协议
-
OSI网络参考模型:全球网络互联标准。 -
TCP/IP网络模型:事实上的国际标准。

- UDP(User Datagram Protocol):用户数据报协议。
- UDP协议通信特点:无连接、不可靠通信、效率高 。
- 不事先建立连接,数据按照包发,一包数据包含:自己的IP、端口、目的地IP、端口和数据(限制在64KB内)等。
- 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的。
- 应用场景:视频直播、语音通话
- UDP协议通信特点:无连接、不可靠通信、效率高 。
- TCP(Transmission Control Protocol):传输控制协议。
- 特点:面向连接、可靠通信。
- TCP的最终目的:要保证在不可靠的信道上实现可靠的数据传输。
- TCP主要有三个步骤实现可靠传输:三次握手建立连接 ,传输数据进行确认,四次挥手断开连接。
- 三次握手建立可靠连接:确保通信的双方收发消息都是没问题的 (全双工)
- 特点:面向连接、可靠通信。

-
三次握手的目的:第一次握手是服务端确保客户端发送消息没有问题,第二次握手是客户端确保服务端接受消息没有问题,且发送消息也没有问题,第三次握手是服务端确保客户端接受消息没有问题
-
四次挥手断开连接的目的:确保通信的双方收发消息都已经完成

- TCP协议:适用于网页、文件下载、支付等
(4)实战
(1)UDP协议实现发送数据和接受数据

- UDP协议实战------一发一收

- UDP协议实战------多发多收

需要注意的是,程序默认是不能多次运行的,需要我们去设置一下:

(2)TCP协议实现发送数据和接受数据



- TCP协议实战------一发一收

- 步骤总结

- TCP协议实战------多发多收

-
支持同时接收多个客户端消息
-
BS架构的原理
----------------------------------------------------------------------用了再学------------------------------------------------------------------------------
19.单元测试
就是针对最小的功能单元:方法,编写测试代码对其进行正确性测试。
- Junit单元测试框架

- 需求
某个系统中有多个业务方法,请使用Junit单元测试框架,编写测试代码,完成对这些方法的正确性测试。


20.反射
(1)认识反射
反射就是:加载类,并允许以编程的方式解剖类中的各种成分(成员变量、方法、构造器等)。

-
获取class对象的三种方式
javaClass c1 = 类名.class 调用class提供方法:public static Class forName(Stringpackage); Object提供的方法:public Class getclass(); Class c3 = 对象.getclass();
