1 面向对象
1.1 特征、多态
00.JDK、JRE、JVM 分别是什么关系?
JDK 即为 Java 开发工具包,包含编写 Java 程序所必须的编译、运行等开发工具以及 JRE
JRE 即为 Java 运行环境,提供了运行 Java 应用程序所必须的软件环境,包含有 Java 虚拟机(JVM)和丰富的系统类库。
JVM 即为 Java 虚拟机,提供了字节码文件(.class)的运行环境支持。
01.什么是面向对象?
面向对象是一种思想,世间万物都可以看做一个对象,面向对象软件开发具有以下优点:
代码开发模块化,更易维护和修改。
代码复用性强。
增强代码的可靠性和灵活性。
增加代码的可读性。
02.面向对象三大特征
封装、继承、多态、抽象
封装,对实体的属性和功能实现进行访问控制,无需知道功能如何实现
继承,子类继承父类后直接使用父类的属性和方法,实现方式有两种:实现继承、接口继承
实现继承:直接使用基类公开的属性和方法,无需额外编码。
接口继承:仅使用接口公开的属性和方法名称,需要子类实现。
多态,是指一个类的同名方法,在不同情况下的实现细节不同。多态机制实现不同的内部实现结构共用同一个外部接口。
多态歧义,一个词语必须根据上下文才有实际的含义(打:打篮球、打水、打架)
方法重载add、方法重写、使用父类作为方法的形参、使用父类作为方法的返回值
-----------------------------------------------------------------------------------------------------
使用多态的一个细节:
(1)当子类重写了父类的方法时,父类的引用会调用子类的重名方法:
(2)当子类和父类的属性重名时,父类的引用会调用父类的重名属性。
-----------------------------------------------------------------------------------------------------
多态的实现离不开继承,
对于父类型,可以有三种形式,即普通的类、抽象类、接口。
对于子类型,则要根据它自身的特征,重写父类的某些方法,或实现抽象类/接口的某些抽象方法。
抽象,Java 支持创建只暴漏接口而不包含方法实现的抽象的类
03.面向对象五大基本原则
单一职责原则:一个类,最好只做一件事,只有一个引起它的变化。
开放封闭原则:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
里氏替换原则:子类必须能够替换其基类
依赖倒置原则:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
接口隔离原则:使用多个小的专门的接口,而不要使用一个大的总接口。
04.面向对象和面向过程的区别
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。
缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:性能比面向过程低
05.面向接口编程的理解
接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,
从而提高系统的可扩展性和可维护性。基于这种原则,很多软件架构设计理论都倡导“面向接口”编程,
而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。
06.一个Java文件里可以有多个类吗(不含内部类)?
一个java文件里可以有多个类,但最多只能有一个被public修饰的类;
如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。
1.2 继承、重载、重写
01.接口是常量值和方法定义的集合。接口是一种特殊的抽象类。
java类是单继承的。classB Extends classA
java接口可以多继承。Interface3 Extends Interface0, Interface1, interface……
02.不允许类多重继承的主要原因是,如果A同时继承B和C,而B和C同时有一个D方法,A如何决定该继承那一个呢?
但接口不存在这样的问题,接口全都是抽象方法继承谁都无所谓,所以接口可以继承多个接口。
注意:
1)一个类如果实现了一个接口,则要实现该接口的所有方法。
2)方法的名字、返回类型、参数必须与接口中完全一致。如果方法的返回类型不是void,则方法体必须至少有一条return语句。
3)因为接口的方法默认是public类型的,所以在实现的时候一定要用public来修饰(否则默认为protected类型,缩小了方法的使用范围)。
03.Java为什么是单继承,为什么不能多继承?
Java是单继承的,指的是Java中一个类只能有一个直接的父类。
Java不能多继承,则是说Java中一个类不能直接继承多个父类。
01.重写与重载的区别
位置 方法名 参数表 返回值 访问修饰符
方法重写 子类 相同 相同 相同或是 不能比父类更严格
方法重载 同类 相同 不相同 无关 无关
方法重载:一般同类/父子维承也可以
要去:
1.方法名相同
2.参数列表不同(类型不同、个数不同、顺序不同)
注意:
1.与返回值无关
2.与参数名无关(仅与参数类型有关)
方法重写(父子继承关系):父类有一个方法,子类重新写了一遍
要求:
1.方法名相同
2.参数列表相同
02.方法重写
对于JVM而言,普通方法是在JVM【运行湖】绑定的,而属性是在【编译期】绑定的,
因此才会有方法重写和属性覆盖二者逻辑不一致的情况,这应该是根本原因
03.重载和重写的区别
重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),
与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,
访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。
01.构造方法能不能重写?
不能,构造方法是不能被继承,而重写的前提是“继承”
02.Java支持动态绑定:
动态绑定是指:在编译期间方法并不会和类绑定在一起,而是在程序运行的过程中,
JVM需要根据具体的实例对像才能确定此时要调用的显围个方法,典型代表:多态、方法重写
1.3 实例化顺序
01.类的实例化顺序
父类静态变量
父类静态代码块
子类静态变量、
子类静态代码块
父类非静态变量(父类实例成员变量)
父类构造函数
子类非静态变量(子类实例成员变量)
子类构造函数
1.4 对象创建方式
01.Java 对象创建的方式?
使用 new 关键字创建对象。
使用 Class 类的 newInstance 方法(反射机制)。
使用 Constructor 类的 newInstance 方法(反射机制)。
使用 clone 方法创建对象。
使用(反)序列化机制创建对象。
1.5 构造方法
01.构造方法有哪些特性?
方法名称和类同名
不用定义返回值类型
不可以写retrun语句
构造方法可以被重载
02.无参构造
如果类中没有任何构造方法,则系统自动提供一个无参构造
如果类中已经存在了任何构造方法,则系统不再提供无参构造
如果给类中编写构造方法,则手动编写一个无参构造,防止报错
03.有参构造
一次性给多个属性赋值
构造方法与setter、getter方法互补:构造方法先使用,setter八getter后补充值
构造方法不能通过方法名直接调用,需要Dog dot=new Dog()实例化
非构造方法(普通方法)可以通过方法名调用
构造方法之间可以相互调用,通过ths,通过参数区分
多个构造方法之间不能循环调用
04.使用构造器时需要记住:
1构造器必须与类同名(如果一个源文件中有多个类,那么构造器必须与公共类同名)
2每个类可以有一个以上的构造器
3.构造器可以有0个、1个或1个以上的参数
4.构造器没有返回值
5.构造器总是伴随着new操作一起调用
05.Java为什么要在类中声明一个无参构造方法?
Java程序编写中,子类的构造方法必定会调用父类的构造方法,
如果在子类的构造方法中没有指定调用父类的某个构造方法,
在实例化子类对象时。子类会默认调用父类的无参构造方法。
如果在父类中没有定义无参构造方法的话,编译会报错。
因此在类中声明一个无参构造函数可以避免其子类在实例化对象时出错。
06.在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是
帮助子类做初始化工作。
07.构造方法能不能重写?
不能,构造方法是不能被继承,而重写的前提是“继承”
构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。
如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。x
1.6 静态方法、实例方法
01.对比
特性 静态方法 实例方法
关键字 static 无
归属 类 对象
调用方式 通过类名或对象调用 通过对象调用
访问权限 只能访问静态变量和静态方法 可以访问实例变量、实例方法、静态变量和静态方法
典型用途 工具类方法、工厂方法 操作对象实例变量、与对象状态相关的操作
生命周期 类加载时存在,类卸载时消失 对象创建时存在,对象销毁时消失
02.扩展
1)静态方法中不能使用ths关键字,因为ths代表当前对象实例,而静态方法属于类,不属于任何实例。
2)静态方法可以被重载(同类中方法名相同,但参数不同),但不能被子类重写(因为方法绑定在编译时已确定)。实例方法可以被重载,也可以被子类重写。
3)实例方法中可以直接调用静态方法和访问静态变量。4)静态方法不具有多态性,即不支持方法的运行时动态绑定。
1.7 内部类
01.一个Java文件里可以有多个类吗(不含内部类)?
一个java文件里可以有多个类,但最多只能有一个被public修饰的类;
如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。
02.内部类包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类
成员内部类
成员内部类定义为位于另一个类的内部,成员内部类可以无条件访问外部类的所有成员属性和成员方法。
局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
匿名内部类
匿名内部类只没有名字的内部类,使用匿名内部类的前提条件:必须继承一个父类或实现一个接口。
静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static
03.内部类与静态内部类的区别?
定义在一个类内部的类叫内部类,包含内部类的类称为外部类。
内部类可以声明public、protected、private等访问限制,可以声明 为abstract的供其他内部类或外部类继承与扩展,
或者声明为static、final的,也可以实现特定的接口。外部类按常规的类访问方式使用内部 类,
唯一的差别是外部类可以访问内部类的所有方法与属性,包括私有方法与属性。
静态类(只有内部类才能被声明为静态类,即静态内部类)
1.只能在内部类中定义静态类
2.静态内部类与外层类绑定,即使没有创建外层类的对象,它一样存在。
3.静态类的方法可以是静态的方法也可以是非静态的方法,静态的方法可以在外层通过静态类调用,
而非静态的方法必须要创建类的对象之后才能调用。
4.只能引用外部类的static成员变量(也就是类变量)。
5.如果一个内部类不是被定义成静态内部类,那么在定义成员变量或者成员方法的时候,是不能够被定义成静态的。
1.8 this与super
01.this:关键字
a.指代当前对象
b.指代当前类
c.指代构造方法this():表示当前类的构造方法,只能放在首行
注意:在新建对象的时候实际上调用了类的无参(没有参数)的构造方法一般默认(在类中可以隐藏)
02.super:关键字
1.只能指代父类对像象
2指代父类的构造方法,只能放在首行
注意:
①子类必须通过super关键字调用父类有参数的构造函数
②使用super调用父类构造器的语句必须是子类构造器的第一条语句
如果子类构造器没有显式地调用父类的构造器,则将自动调用父类的默认(没有参数)的构造器。
如果父类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用父类的构造器,则jva编译器
报告错误
03.this与super的区别
相同点:
super()和this()都必须在构造函数的第一行进行调用,否则就是错误的
this()和super()都指的是对象,所以,均不可以在static环境中使用。
不同点:
super()主要是对父类构造函数的调用,this()是对重载构造函数的调用
super()主要是在继承了父类的子类的构造函数中使用,是在不同类中的使用;this()主要是在同一类的不同构造函数中的使用
1.9 访问修饰符
01.Java语言为我们提供了三种访问修饰符,即private、protected、public,
在使用这些修饰符修饰目标时,一共可以形成四种访问权限,即private、default、protected、public,
注意在不加任何修饰符时为default访问权限。
类内部 本包 子类 外部包
public √ √ √ √
protected √ √ √ ×
default √ √ × ×
private √ × × ×
02.区别
public:可以被所有其他类所访问。
private:只能被自己访问和修改。
protected:自身,子类及同一个包中类可以访问。
default(默认):同一包中的类可以访问,声明时没有加修饰符,认为是default。
1.10 抽象类、接口
01.抽象类不能实例化(newX0)
原因:抽象类中可能存在抽象方法,而抽象方法没有方法体
02.抽象类到底有什么好?
1.缺点。抽象类必须通过子类继承才有意义,而继承会增加父类和之间的耦合度,与“高内聚低耦合”的设计原则相违背(这也是很多人反对使用“继承”的原因)。因此,很多人推荐使用接口。
2.优点。模板化编程。在公司里,为了统一出高标准。往往由“高手”先通过抽象类写一个模板,然后新人们可以仿照这个模板编写自己的模块代码。
03.接口和抽象类有什么区别?
a.从使用方式上来说,二者有如下的区别:
接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。
接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。
接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,
通过实现多个接口可以弥补Java单继承的不足。
b.相同点
接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
04.接口中可以有构造函数吗?
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。
接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、
内部类(包括内部接口、枚举)定义。
05.在Java语言中,abstract class和interface是支持抽象类定义的两种机制。
抽象类:用来捕捉子类的通用特性的。
接口:抽象方法的集合。
07.JDK8前,接口中可以定义变量和方法
public interface MyInterface {
public static final int field1 = 0; // 变量:默认修饰符(public、static、final)
int field2 = 0; // 变量:等价上述写法
public abstract void method1(int a) throws Exception; // 方法:默认修饰符(public、abstract)
void method2(int a) throws Exception; // 方法:等价上述写法
}
08.JDK8后,接口中的默认方法,并提供默认实现,【可以对该默认方法重写】
定义:default
调用:Vehicle.super.print();
作用:新增的默认方法在实现类中直接可用
------------------------------------------------------------------------------------------------------
在Java8之前,在基于抽象的设计中,接口只能有抽象方法,一个接口有一个或多个实现类;
【若接口要增加某个方法,则所有实现类都要新增这个方法的实现,否则就就不满足接口的约束】
【默认接口方法,就是为解决这一问题,实现类可以不用修改继续使用,并且新增的默认方法在实现类中直接可用】
【妥协:维护现有代码的向后兼容性时,静态方法和默认方法是一种很好的折衷,逐步为接口提供附加功能,而不破坏实现类】
------------------------------------------------------------------------------------------------------
接口默认方法,扩展接口而不必担心破坏实现类
接口默认方法,缩小了接口和抽象类之间的差异。
接口默认方法,无需创建基类,由实现类自己选择覆盖哪个默认方法实现。
接口默认方法,增强了Java 8中的Collections API以支持lambda表达式。
接口默认方法,默认方法不能为java.lang.Object中的方法,因为Object是所有类的基类,这种写法将毫无意义
------------------------------------------------------------------------------------------------------
如果子类没有重写父接口默认方法的话,会直接继承父接口默认方法的实现;
如果子类重写父接口默认方法为普通方法,则与普通方法的重写类似;
如果子类(接口或抽象类)重写父接口默认方法为抽象方法,那么所有子类的子类需要实现该方法;
------------------------------------------------------------------------------------------------------
多个默认方法:一个类实现了多个接口,且这些接口有相同的默认方法,需要【重写默认方法,或指定哪个接口的默认方法】
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
}
public interface FourWheeler {
default void print(){
System.out.println("我是一辆四轮车!");
}
}
------------------------------------------------------------------------------------------------------
public class Car implements Vehicle, FourWheeler { // 解决办法1:重写默认方法
default void print(){
System.out.println("我是一辆四轮汽车!");
}
}
public class Car implements Vehicle, FourWheeler { // 解决办法2:指定哪个接口的默认方法
public void print(){
Vehicle.super.print();
}
}
09.JDK8后,接口中的静态方法,并提供默认实现,【无法对该静态方法重写】【仅对接口方法可见,实例对象无法访问】
定义:static关键字
调用:Vehicle.blowHorn();
作用:将与相关方法内聚到接口,提高内聚性,无需创建额外对象
------------------------------------------------------------------------------------------------------
【接口提供一种简单的机制,允许通过将相关的方法内聚在接口中,而不必创建新的对象】
【虽然抽象类,也可以“类似接口,提供静态方法,来提高内举性”,但主要区别在于抽象类可以有构造函数、成员变量和方法】
【推荐:把只和接口相关的静态utility方法放在接口中(提高内聚性),而无需额外创建一些utility类专门处理逻辑】
------------------------------------------------------------------------------------------------------
接口静态方法,接口的一部分,实例对象无法直接访问
接口静态方法,非常适合提供有效的方法,例如null检查,集合排序等
接口静态方法,不允许被实现类覆盖,来提供安全性
------------------------------------------------------------------------------------------------------
public interface MyInterface {
default void log(String str) {
if (!isEmpty(str)) {
System.out.println("接口默认log()方法:" + str);
}
}
static boolean isEmpty(String str) {
System.out.println("对“接口默认log()方法”是否为null进行检查");
return str == null ? true : "".equals(str) ? true : false;
}
}
public class Demo implements MyInterface {
public static void main(String[] args) {
Demo demo = new Demo(); // MyInterface接口中,log()调用“静态isEmpty()”
demo.log("");
demo.log("test");
MyInterface.isEmpty(""); // 实例对象无法方法,但可以使用“MyInterface.isEmpty()”
}
}
------------------------------------------------------------------------------------------------------
对“接口默认log()方法”是否为null进行检查
对“接口默认log()方法”是否为null进行检查
接口默认log()方法:test
10.JDK8,函数式接口,(java.util.function.Consumer、Supplier、Function、predicate)
有且只有一个抽象方法的接口,称为函数式接口
@Override:检查“重写父类或实现接口的方法”的正确性 可以不添加
@FunctionalInterface:检查“避免在函数式接口中,意外添加其他的抽象方法”的正确性 可以不添加
------------------------------------------------------------------------------------------------------
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); // @FunctionalInterface保证仅有1个抽象方法(public abstract)
public boolean equals(Object object); // 并非抽象类方法(public boolean)
default Comparator<T> reversed() { // JDK8,默认方法
return Collections.reverseOrder(this);
}
public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) { // JDK8,静态方法
return new Comparators.NullComparator<>(true, comparator);
}
}
1.11 static、final
01.static关键字可以修饰成员变量、成员方法、初始化块、内部类,被static修饰的成员是类的成员,它属于类、不属于单个对象
类变量:被static修饰的成员变量叫类变量(静态变量)。
类变量属于类,它随类的信息存储在方法区,并不随对象存储在堆中,
类变量可以通过类名来访问,也可以通过对象名来访问,但建议通过类名访问它。
类方法:被static修饰的成员方法叫类方法(静态方法)。
类方法属于类,可以通过类名访问,也可以通过对象名访问,建议通过类名访问它。
静态块:被static修饰的初始化块叫静态初始化块。
静态块属于类,它在类加载的时候被隐式调用一次,之后便不会被调用了。
静态内部类:被static修饰的内部类叫静态内部类。
静态内部类可以包含静态成员,也可以包含非静态成员。
静态内部类不能访问外部类的实例成员,只能访问外部类的静态成员。
外部类的所有方法、初始化块都能访问其内部定义的静态内部类。
02.final关键字可以修饰类、方法、变量,以下是final修饰这3种目标时表现出的特征:
final类:final关键字修饰的类不可以被继承。
final方法:final关键字修饰的方法不可以被重写。
final变量:final关键字修饰的变量,一旦获得了初始值,就不可以被修改。
03.static修饰的类能不能被继承?
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。
因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。
04.final finally finalize区别
final主要用于修饰类,变量,方法
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个属于Object类的一个方法,该方法一般由垃圾回收器来调用,当我们调用System.gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾,但Java语言规范并不保证inalize方***被及时地执行、而且根本不会保证它们会被执行。
1.12 反射
01.反射
a.定义
关键:【反射是在运行状态中】
【对于任意一个类,都能够知道这个类的所有属性和方法】
【对于任意一个对象,都能够调用它的任意一个属性和方法】
反射是Java提供的一种功能,通过反射可以无视Java的一些限制访问机制,直接使用某个类的私有变量或私有方法。
b.步骤
首先,通过对象得到它所对应的类型,
然后,通过Class类提供的一些方法得到对应的变量或者方法,
最后,再通过这些Field类和Method类直接访问某个对象的某个变量或者方法,而不是通过一般的通过.操作符进行访问
c.功能
在运行时,判断任意一个对象所属的类
在运行时,构造任意一个类的对象
在运行时,判断任意一个类所具有的成员变量和方法
在运行时,调用任意一个对象的方法
生成动态代理
a.【类、对象与类对象】
在Java中,类是是对具有一组相同特征或行为的实例的抽象并进行描述,对象则是此类所描述的特征或行为的具体实例;
作为概念层次的类,其本身也具有某些共同的特性,如类名称、由类加载器去加载,都具有包,具有父类,属性和方法等;
于是,Java中有专门定义了一个类(Class类),去描述其他类所具有的这些特性,
因此,从此角度去看,类本身也都是属于Class类的对象。为与经常意义上的对象相区分,在此称之为”类对象”。
b.Object类与Class类
Object类和Class类没有直接的关系;
Object类是一切java类的父类,对于普通的java类,即便不声明,也是默认继承了Object类;
Class类是用于java反射机制的,一切java类,都有一个对应的Class对象。
02.反射
a.反射入口:获取类的对象
Class.forName("全类名") --person对象
类名.class --person对象
对象名.getClass() --person对象
b.构造方法:无父类构造,无法继承
Constructor<?>[] getConstructors() --所有公共的构造方法
Constructor<?>[] getDeclaredConstructors() --所有公共、私有的构造方法
Constructor<T> getConstructor(类<?>... parameterTypes) --指定公共
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) --指定私有
c.属性
Field[] getFields() --所有公共的属性(父类、本类
Field[] getDeclaredFields() --所有公共、私有的属性(本类)
Field getField(String name) --指定公共
Field getDeclaredField(String name) --指定私有
d.方法
Method[] getMethods() --所有公共的方法(本类、父类、接口)
Method[] getDeclaredMethods() --所有公共、私有的方法(本类)
Method getMethod(String name, 类<?>... parameterTypes) --指定公共
Method getDeclaredMethod(String name, 类<?>... parameterTypes) --指定私有
e.父类
Class<? super T> getSuperclass() --父类只有一个,“单继承、多实现”
f.接口
Class<?>[] getInterfaces() --接口拥有多个,“单继承、多实现”
g.类对象 -> 对象实例
T newInstance() --创建由此类对象表示的类的新实例
03.场景一:JDBC连接过程:
导包
获取Driver的实现类对象。
注册驱动。
提供需要连接的数据库信息。
创建Connection对象,获取连接。
04.场景二:反射改String
虽然String长度不可变,但是可以通过反射暴露修改String
String str = "hello";
//源码中的value数组
Field filed = String.class.getDeclaredField("value");
//设置可访问
filed.setAccessible(true);
//得到value数组
char[] chars = (char[])filed.get(str);
//修改
chars[0] = 'w';
System.out.println(str);
05.java反射的作用
反射(Reflection)是Java 程序开发语言的特征之一,它允许运行中的Java 程序获取自身的信息,
并且可以操作类或对象的内部属性。 通过反射机制,可以在运行时访问Java 对象的属性,方法,构造方法等。
06.哪里会用到反射机制?
使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。
07.动态代理是什么?
动态代理类(基于接口实现) 静态代理是代理类在代码运行前已经创建好,并生成class文件;
动态代理类是代理类在程序运行时创建的代理模式。
动态代理类的代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。
1.13 堆栈
01.堆区
专门用来保存对象的实例(new 创建的对象和数组),实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中)
存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身.
一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
02.栈区
对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区
每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.
03.静态区/方法区:
方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域
栈内存 私有 线程
堆内存 共享 线程
堆是线程共享的内存区域,栈是线程独享的内存区域。
堆中主要存放对象实例,栈中主要存放各种基本数据类型、对象的引用。
04.说一说,heap和stack有什么区别。
1.heap是堆,stack是栈。
2.stack的空间由操作系统自动分配和释放,heap的空间是手动申请和释放的,heap常用new关键字来分配。
3.stack空间有限,heap的空间是很大的自由区。在Java中,若只是声明一个对象,则先在栈内存中为其分配地址空间,若再new一下,实例化它,则在堆内存中为其分配地址。
4.举例:数据类型 变量名;这样定义的东西在栈区。如:Object a =null; 只在栈内存中分配空间new 数据类型();或者malloc(长度); 这样定义的东西就在堆区如:Object b =new Object(); 则在堆内存中分配空间
05.堆内存
1.什么是堆内存?
堆内存是是Java内存中的一种,它的作用是用于存储Java中的对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。
2.堆内存的特点是什么?
第一点:堆其实可以类似的看做是管道,或者说是平时去排队买票的的情况差不多,所以堆内存的特点就是:先进先出,后进后出,也就是你先排队,就能先买到票
第二点:堆可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,但缺点是,由于要在运行时动态分配内存,存取速度较慢。
3.new对象在堆中如何分配?
分配是动态分配的,回收是由Java虚拟机的自动垃圾回收器来管理
06.栈内存
1.什么是栈内存
栈内存是Java的另一种内存,主要是用来执行程序用的,比如:基本类型的变量和对象的引用变量
2.栈内存的特点
第一点:栈内存就好像一个矿泉水瓶,像里面放入东西,那么先放入的沉入底部,所以它的特点是:先进后出,后进先出
第二点:存取速度比堆要快,仅次于寄存器,栈数据可以共享,但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性
3.栈内存分配机制
栈内存可以称为一级缓存,由垃圾回收器自动回收
07.栈和堆的区别
JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
1.栈堆差异点
(1)堆内存用来存放由new创建的对象和数组。
(2)栈内存用来存放基础数据或静态变量等
(3)堆是先进先出,后进后出
(4)栈是后进先出,先进后出
2.栈堆相同点
(1)都是属于Java内存的一种
(2)系统都会自动去回收它,但是对于堆内存一般开发人员会断开引用让系统回收它
1.14 Error异常
01.区别
1什么叫做“运行时异常“?代码在编辑(编译阶段)时不报错,运行时才报错。语法上,可以选择性处理
2什么叫做“非运行时异常”?代码在编辑(编译阶段)时报错。语法上,必须处理
02.Error、Exception
Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error、Exception。
Error是错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。
Exception是异常,它被分为两大类,分别是Checked异常和Runtime异常。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try...catch块来实现。
03.最常见的5个RuntimeException?
ClassCastException类转换异常,
IllegalArgumentException非法参数异常,
IndexOutOfBoundsException数组越界异常,
NullPointerException空指针异常,
ArrayStoreException数据存储异常
04.处理异常:try catch或者throws
1.try catch
自己(当前方法)能够处理,使用try catch
场景:辅导员能够处理的“感冒、发烧”try catch
2.throws
自己(当前方法)不能处理,使用throws,上交给上级(方法调用处)处理
场景:辅导员不能够处理的“武汉疫情”throws上报领导
05.处理异常:try catch或者throws
try
将可能发生异常的代码,用包裹起来
如果try中的代码的确发生了异常,则程序不再执行try中异常之后的代码,而是直接跳到catch中执行
catch
捕获特定类型的异常
两个catch(已知类型的异常可以捕获,但是未知类型的异常“无法捕获其准确的类型,需要交由Exception e捕获”)
捕获类型的范围大小先小范围,后大范围
finally
无论正常,还是异常,始终都会执行的代码
不论执行完try,还是执行完catch,最终都会执行finally的代码
无论如何都会执行? 即使遇到return,也仍然先执行finally,再执行return
什么时候不会执行finally? 除非虚拟机关闭,才不会执行finally
throws
自己(当前方法)不能处理,使用throws,上交给上级(方法调用处)处理
场景:辅导员不能够处理的“武汉疫情”throws上报领导
throw一般和自定义异常一起使用
JDK中自带了很多类型的异常,但如果这些内置的异常仍然不能满足项目的需求,那么就需要创建自定义异常
如何编写自定义异常?1.继承Exception,构造方法调用super("异常信息")
2.使用throw声明一个自定义异常,并且进行try catch和throws处理异常
06.throw 和 throws 的区别是什么?
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,
可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
07.throws关键字和throw关键字在使用上的几点区别如下:
throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
08.final、finally、finalize 有什么区别?
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
1.15 值传递、址传递
01.形参与实参区别
形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。
因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外。
实参:就是实际参数,用于调用时传递给方法的参数
02.值传递、址传递
java中并不存在引用调用,java程序设计语言确实是采用了按值调用,即call by value。也就是说方法得到的是所有参数值的一个拷贝,方法并不能修改传递给它的任何参数变量的内容。
java函数在传递引用数据类型时,也只是拷贝了引用的值罢了,之所以能修改引用数据是因为它们同时指向了一个对象,但这仍然是按值调用而不是引用调用。
总结:
一个方法不能修改一个基本数据类型的参数(数值型和布尔型)。
一个方法可以修改一个引用所指向的对象状态,但这仍然是按值调用而非引用调用。
上面两种传递都进行了值拷贝的过程。
值传递:是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递:是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
03.全部
1.形参:用来接收调用该方法时传递的参数。只有在被调用的时候才分配内存空间,一旦调用结束,就释放内存空间。因此仅仅在方法内有效。
2.实参:传递给被调用方法的值,预先创建并赋予确定值。
3.传值调用:传值调用中传递的参数为基本数据类型,参数视为形参。
4.传引用调用:传引用调用中,如果传递的参数是引用数据类型,参数视为实参。在调用的过程中,将实参的地址传递给了形参,形参上的改变都发生在实参上。
04.区别
值传递
提供的值
址传递
提供的变量地址
05.值传递和引用传递有什么区别
值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
1.16 克隆:浅拷贝、深拷贝
01.方法
protected native Object clone() throws CloneNotSupportedException;
02.解释
clone函数返回的是一个引用(“浅拷贝”),指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。
03.调用
这是一个protected方法,作用在本类、子类,同包类中;若要在某个类中使用该方法,需要重写重写clone()方法:
某个类如果要使用clone()方法进行自身复制,就必须实现Cloneable接口,否则就会抛出CloneNotSupportedException
但是这个接口本身并没有任何方法,真正的要实现复制还是要靠重写父类的clone()方法,接口只是起了标识作用;
public class A implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
-------------------------------------------------------------------------------------------------
所有数组都被认为是实现接口Cloneable,并且数组类型T[]的clone方法的返回类型是T[],T是任何引用或原始类型;
否则该方法将创建该对象的类的新实例,并将其所有字段初始化为完全符合该对象的相应字段的内容,就像通过赋值一样,
这些字段的内容本身不被克隆。因此,该方法执行该对象的“浅拷贝”,而不是“深度拷贝”操作。
04.浅拷贝
如果【成员变量是引用数据类型】,复制后的对象与原始的对象【共用同一个成员变量】:
对【复制后的对象中成员变量的更改】也会出现在【原始的对象】中,因为它们共同引用的是堆中的同一个实例
-------------------------------------------------------------------------------------------------
public class A implements Cloneable{
private int id; // 原生数据类型,默认深拷贝,不受影响
private String username; // 引用数据类型,默认浅拷贝,深拷贝【手动增加对成员变量的复制】
private String password; // 引用数据类型,默认浅拷贝,深拷贝【手动增加对成员变量的复制】
private B b = new B(); // 引用数据类型,默认浅拷贝,深拷贝【手动增加对成员变量的复制】
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class B implements Cloneable{
int a = 1;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
05.深拷贝
如果【成员变量是引用数据类型】,复制后的对象与原始的对象【不共用同一个成员变量】:
【复制这个对象的所有成员变量的实例,而不是复制引用】,默认clone()无法实现,需要【手动增加对成员变量的复制】
-------------------------------------------------------------------------------------------------
public class A implements Cloneable{
private int id; // 原生数据类型,默认深拷贝,不受影响
private String username; // 引用数据类型,默认浅拷贝,深拷贝【手动增加对成员变量的复制】
private String password; // 引用数据类型,默认浅拷贝,深拷贝【手动增加对成员变量的复制】
private B b = new B(); // 引用数据类型,默认浅拷贝,深拷贝【手动增加对成员变量的复制】
@Override
protected Object clone() throws CloneNotSupportedException {
A a = (A) super.clone();
a.b = (B) b.clone();
return a;
}
}
public class B implements Cloneable{
int a = 1;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
1.17 序列化
01.序列化和反序列化
a.图示
序列化(java.io.ObjectOutputStream 类的 public final void writeObject(Object obj) throws IOException )
-------→
对象的状态(成员变量,不包括类中的静态变量) byte[]字节数组
←-------
反序列化(java.io.ObjectInputStream 类的 public final Object readObject() throws IOException )
b.技术选型
thrift、protobuf --对性能敏感,对开发体验要求不高
hessian --对开发体验敏感,性能有要求
jackson、gson、fastjson --序列化后的数据有良好的可读性(转为json、xml形式)
c.原因
【对象的寿命,随着JVM的停止允许,而丢失状态】
【有时候需要把在内存中的各种对象的状态(也就是实例变量,不是方法)保存下来,并在需要的时候,再将对象恢复】
【虽然,我们可以通过各种各样的方法来保存“对象的状态”,但JAVA提供了一种保存对象状态的机制,那就是“序列化”】
----------------------------------------------------------------------------------------------------
【有些对象可能很重要且占用不少内存,但“可能暂时不使用该对象”,若直接放入内存显然浪费、若丢弃又需要额外创建对象】
【“一种折中的方法”,将“对象的状态”暂时放入“文件”、“数据库”,然后根据需要进行“磁盘读取”,这就是“序列化”】
d.序列化的优点
【保存到“文件”“数据库”】:将一个已经实例化的类转成文件存储,【隔一段时间后】,可以恢复【类的所有变量和状态】
【方便在网络中进行传送】:对象、文件、数据等格式,【序列化后,无论原来是什么,都可以变为byte[]字节流】
【RMI(远程方法的调用)】:分布式对象,利用【对象序列化】运行远程主机上的服务,就像【在本地机上运行对象】一样
e.Java序列化算法
所有保存到磁盘的对象都有一个序列化编码号。
当程序试图序列化一个对象时,会先检查此对象是否已经序列化过。
若对象从未被序列化过,才会将此对象序列化为字节序列输出;如果此对象已经序列化过,则直接输出编号即可。
f.Java序列化的缺陷
无法跨语言:Java序列化目前只适用于Java语言,若两种不同语言使用“序列化”进行通讯,可能无法完成
容易被攻击:一个实例能直接从byte[]数组创建,而不经过构造方法,可以攻击“将序列化后的对象传输到程序中反序列化”
序列化后的流太大:ObjectOutputStream 实现对象转二进制编码,编码后的数组太大,影响存储和传输
序列化性能太差:速度也是体现序列化性能的重要指标,如果速度过慢,会影响网络通信的效率,从而增加系统的响应时间
序列化编程限制:需要时刻关注两点,【实现Serializable接口(序列化标志)、serialVersionUID(版本号)】
过程复杂开销大:若一个对象的“成员变量是容器类对象,而容器中的元素也是'容器对象'”,则序列化过程较复杂,开销也较大
02.Serializable 接口:标识接口,没有方法
a.序列化类的要求
被序列化的类必须属于【Enum、Array和实现Serializable接口】中的一种,否则将抛出NotSerializableException异常
b.默认序列化机制
【不仅会序列化“当前对象本身”,还会对“其父类的字段”,“该对象引用的其它对象(成员变量是对象类型)”也进行序列化】;
【注意,此处的“父类、引用的其他对象”,也必须满足序列化要求,即Enum、Array 和 Serializable中一种】
【不包括类中的静态变量,“序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量”】
c.serialVersionUID
serialVersionUID 字段必须是 static final long 类型,建议使用 private 修饰符(仅声明,子类继承该字段无意义)
数组类不能声明一个明确的 serialVersionUID,因此它们具有默认的计算值,但是数组类没有这种 serialVersionUID 要求
----------------------------------------------------------------------------------------------------
serialVersionUID 是 Java 为【每个序列化类】产生的版本标识。
它可以用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。
如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,会抛出InvalidClassException
----------------------------------------------------------------------------------------------------
在某些场合,希望【类的不同版本对序列化兼容】,因此需要确保【类的不同版本具有“相同”的serialVersionUID】
在某些场合,不希望【类的不同版本对序列化兼容】,因此需要确保【类的不同版本具有“不同”的serialVersionUID】
----------------------------------------------------------------------------------------------------
若没有显式声明 serialVersionUID,【默认 serialVersionUID 值根据类名、接口名、成员方法及属性生成】
虽然可以生成不同的值,但【不同JDK编译可能生成不同值,会导致异常】,因此建议【给每个类指定serialVersionUID值】
d.transient
当某些变量不想被序列化,同是又不适合使用static关键字声明,那么此时就需要用transient关键字来声明该变量
被transient修饰的属性是默认值:对于引用类型是null;基本类型是0;boolean类型是false
public class SerializeDemo02 {
static class Person implements Serializable {
transient private Integer age = null;
// 略
}
}
// Output: name: Jack, age: null, sex: MALE
e.可选的自定义序列化
writeObject:自定义序列化规则,比如哪些属性、依据的规则
readObject:自定义反序列化规则,比如哪些属性、依据的规则
readObjectNoData:使用不同类接收反序列化对象,或序列化流被篡改时,调用readObjectNoData()初始化反序列化对象
f.更彻底的自定义序列化
readResolve、writeReplace访问修饰符,可以是private、protected、public
如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。
对于final修饰的类重写readResolve,可以是public;非final修饰的类重写readResolve,不建议public,推荐private
03.Externalizable 接口:继承 Serializable 接口,两个抽象方法(“writeExternal()”、"readExternal()")
a.失效问题
可序列化类实现 Externalizable 接口之后,基于 Serializable 接口的默认序列化机制就会失效
b.相比 Serializable 接口
若直接使用 Externalizable 接口,
默认【writeExternal、readExternal未作任何处理,则“该序列化行为将不会保存/读取任何一个字段”】
序列化时,【先调用“该类的无参构造方法,且类型必须为public”创建新对象,然后“将'待保存字段'分别填充到新对象中”】
c.替代方案
实现 Serializable 接口,
并添加 writeObject(ObjectOutputStream out) 与 readObject(ObjectInputStream in) 方法。
序列化和反序列化过程中会自动回调这两个方法。
d.单例模式的序列化问题
单例模式(某个类的创建的唯一性),如果该类进行“序列化”,会出现“单例模式创建的对象,与序列化后的对象,并不相等”,
因此,为了保证“单例类创建类的唯一性”,需要重写readResolve()方法,直接返回 Person 的单例对象
04.序列化的类
a.String
public final class String implements java.io.Serializable,Comparable<String>,CharSequence {
private static final long serialVersionUID = -6849794470754667710L;
}
b.ArrayList、HashMap、LinkedList、TreeSet
ArrayList transient Object[] elementData;
HashMap transient Node<K,V>[] table;
LinkedList transient Node<E> first;
TreeSet private transient NavigableMap<E,Object> m;
但是,ArrayList、HashMap、LinkedList等数据存储字段,修饰符都是transient,但可以正常序列化、反序列化;
实际上,各个集合类型,对于序列化和反序列化是有单独的实现的,并没有采用虚拟机默认的方式;
05.Java 序列化
a.Serializable 接口
b.Externalizable 接口
c.替代方案:Serializable 接口,添加writeObject(ObjectOutputStream out)、readObject(ObjectInputStream in)
d.单例模式:需要重写readResolve()方法保证“单例模式创建的对象,与序列化后的对象”是同一个,否则会破坏单例原则
06.序列化、反序列化、对象序列化
序列化:就是将对象转化成字节序列的过程。
反序列化:就是讲字节序列转化成对象的过程。
对象序列化成的字节序列会包含对象的类型信息、对象的数据等,包含描述这个对象的所有信息
1.18 io流
01.java中 IO流分为几种?
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。
02.Java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系,
Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
03.BIO,NIO,AIO 有什么区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
04.以文件操作为例,主要的操作流程如下:
1 使用File类打开一个文件
2 通过字节流或字符流的子类,指定输出的位置
3 进行读/写操作
4 关闭输入/输出
05.字节流
字节流主要是操作byte类型数据,以byte数组为准,
主要操作类就是OutputStream、InputStream。两者都是抽象类,是字节输入、输出流的父类。以InputStream为例,最常用到的子类有:FileInputStream,ByteArrayInputStream,ObjectInputStream,BufferedInputStream、DataInputStream、PushbackInputStream。
06.字符流
在程序中一个字符等于两个字节,java提供了Reader、Writer两个专门操作字符流的类。两者同样是抽象类。已Reader为例,其常用的子类有:BufferedReader,CharArrayReader,FilterReader,InputStreamReader,PipedReader,StringReader 。
其中FilterReader是抽象类,其实例化的类为PushbackReader。
07.字节流和字符流的区别?
字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;
字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。
字节流与字符流主要的区别是他们的的处理方式
08.什么是IO多路复用?
IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
没有文件句柄就绪就会阻塞应用程序,交出CPU。
09.NIO的缓冲区
通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、套接字)的连接。
若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
简而言之,Channel 负责传输, Buffer 负责存储。
Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
10.Java NIO的Channel类似流,是用于传输数据的数据流,但有不同:
既可从Channel读数据,也可写数据到Channel。但流的读写通常单向
Channel可异步读写
Channel中的数据总要先读到一个Buffer,或从一个Buffer中写入
1.19 jvm
01.为什么Java代码可以实现一次编写、到处运行?
JVM(Java虚拟机)是Java跨平台的关键。
在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。
编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行;
跨平台的是Java程序、而不是JVM,JVM是用C/C++开发的软件,不同平台下需要安装不同版本的JVM。
02.JVM、JRE及JDK的关系
JDK是Java的开发工具,JRE是Java程序运行所需的环境,JVM是Java虚拟机.它们之间的关系是JDK包含JRE和JVM,JRE包含JVM
03.JVM主要由四大部分组成:
ClassLoader(类加载器),
Runtime Data Area(运行时数据区,内存分区),
Execution Engine(执行引擎),
Native Interface(本地库接口)
04.jdk自带工具
jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具
jmap:Java内存映像工具
jhat:虚拟机堆转储快照分析工具
jstack:Java堆栈跟踪工具
jinfo:Java配置信息工具
VisualVM:图形化工具,可以得到虚拟机运行时的一些信息:内存分析、CPU分析等等,在jdk9开始不再默认打包进jdk中。
05.什么是 Java 中的 JIT(Just-In-Time)编译?
我们都知道Jva默认是解释执行,但是解释执行的效率确实比不上编译执行。
因此Java就搞了个JTT(Just-ln-Time)编译器,它在Java程序运行的时候,发现热点代码时,就会将字节码转为机器码,因为这种转换是在程序运行时即时进行的,因此得名"Just-ln-Time"。
06.什么是 Java 的 AOT(Ahead-Of-Time)编译?
AOT(Ahead-Of-Time)它和JIT(Just-ln-Time)编译相对。
JIT是在Java运行时将一些代码编译成机器码,而AOT则是在代码运行之前就编译成机器吗,也就是提前编译。
提前编译的好处是减少运行时编译的开销,且减少程序启动所需的编译时间,提高启动速度。
-------------------------------------------------------------------------------------------------------------
01.类的生命周期
1.类的加载:①查找并加载类的二进制数据(class.文件) ②硬盘上的class文件加载到jvm内存中
2.连接:确定类与类之间的关系
1.验证:.class正确性校验
2.准备:static静态变量分配内存,并赋初始化默认值
<1>在准备阶段,JVM中只有类,没有对象。
<2>初始化顺序:static->非static->构造方法
3.解析:把类中符号引用,转为直接引用
3.初始化:给static变量赋予正确的值
4.使用:对像的初始化、对象的垃圾回收、对象的销毁
5.卸载
02.JVM的内存区域是如何划分的?
Java虚拟机运行时数据区分为程序计数器、虚拟机栈、本地方法栈、堆、方法区。
1)程序计数器
作为当前线程执行字节码的行号指示器,简单理解就是标记执行到第一行了,每个线程都有自己的程序计数器。
2)虚拟机栈
每个线程执行时在虚拟机栈中都会有自己的栈帧,存储局部变量、方法出口、操作数栈等信息,在方法调用栈帧入栈,方法返回,栈帧出栈。
3)本地方法栈
与虚拟机栈类似,它是用于本地方法的调用,即native方法。
4)堆
堆主要存放的就是平时new的对象实例和数组,按垃圾回收划分,堆可以分为新生代、老年代、永久代(Jva8后被元空间取代,不在堆内了)。
5)方法区
方法区主要存储类结构、常量、静态变量、即时编译后的代码等信息,Java8后存在元空间(元空间可以认为是方法区的一个实现),存储在堆外内中。
程序计数器、虚拟机栈、本地方法栈这3个区域是线程私有的,会随线程消亡而自动回收,所以不需要管理。
而堆和方法区是线程共享的,所以垃圾回收器会关注这两个地方。
03.Java 中堆和栈的区别是什么?
栈:主要用于存储局部变量、操作数栈、方法返回地址等数据,它空间的分配和回收是自动的,每个线程都拥有自己栈空间,线程之间不共享。
堆:存储new的Jva对象实例与数组,堆的内存是通过手动触发器gc或者垃圾回收器自动回收,堆是线程共享的。
04.什么是 Java 中的常量池?
常量池其实就是方法区的一部分,全称应该是运行时常量池(runtime constant pool)),主要用于存储字面量和符号引用等编译期产生的一些常量数据。
比如一些字符串、整数、浮点数都是字面量,源代码中一个写了一个固定的值的都叫字面量。
比如你代码写了一个String s='aa';那么aa就是字面量,存储在常量池当中。
符号引用指的是字段的名称、接口全限定名等等,这些都算符号引用。
常量池的好处是减少内存的消耗,比如同样的字符串,常量池仅需存储一份。
且常理池在类加载后就已经准备好了,这样程序运行时可以快速的访问这些数据,提升运行时的效率。
05.类的初始化
1.类的使用方式:JVM只会在“首次主动使用”一个类/接口时,才会初始化它们。
1.主动使用
1.new构造类的使用,加载该类的静态方法
2.访问类/接口的静态成员(属性、方法),加载该类的静态方法
3.使用Class.forName("init.B")执行反射时使用的类(B类
4.初始化一个子类时,该子类的父类也会被初始化
5.动态语言在执行所涉及的类也会被初始化(动态代理)
2.被动使用
除了主动以外,其他都是被动使用。
2.主动使用中的静态成员问题
1.常量产生的时机
final static称为“常量”,不会被初始化
常量产生时机:①时机:编译期间
②地点:(调用这个常量的方法所在类(Test2))的常量池,并不是类A的常量池
2.主动使用中的静态成员问题
1.类的静态成员(属性、方法),会初始化类的静态资源
2.不是常量,因此也会被初始化
3.常量值是一个随机值,则会被初始化 (安全考虑)
4.常量值是一个随机值,则会被初始化 (安全考虑)
5.初始化一个子类中,该子类的父类也会被初始化
06.JVM的四种引用方式分别是强引用、软引用、弱引用、虚引用,具体含义如下:
强引用:这是Java程序中最常见的引用方式,即程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
软引用:当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象。当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。
弱引用:弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
虚引用:虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列联合使用。
07.JVM是如何运行的?
JVM的装入环境和配置
装载JVM
初始化JVM,获得本地调用接口
运行Java程序
08.JVM中的堆一般分为三部分,新生代、老年代和永久代。
新生代:主要是用来存放新生的对象。一般占据堆空间的1/3,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
老年代:老年代的对象比较稳定,所以MajorGC不会频繁执行。
永久代:内存的永久保存区域,主要存放Class和Meta(元数据)的信息。
09.双亲委派
在 Java 中,双亲委派模型是一种类加载机制,用于确保 Java 类加载的一致性和安全性。其主要特点如下:
1.委派机制:
类加载器在加载类时,会先委派给其父类加载器进行加载。
如果父类加载器无法加载,则由当前加载器尝试加载。
2.加载流程:
当一个类加载器收到类加载请求时,它首先将请求委派给它的父类加载器。
父类加载器递归地遵循相同的委派规则,直到找到或抛出 ClassNotFoundException。
3.确保一致性:
双亲委派模型保证了 Java 类的唯一性(同一个类只会被加载一次)。
例如,java.lang.Object 类始终由 Bootstrap ClassLoader 加载,不管其他类加载器如何。
10.JDK8的时候一共有三种类加载器:
1)启动类加载器(Bootstrap ClassLoader),它是属于虚拟机自身的一部分,用C++实现的(JDK9后用java实现),主要负责加载<AVA HOME>ib目录中或被-bootclasspath指定的路径中的并且文件名是被虚拟机识别的文件,它是所有类加载器的父亲。
2)扩展类加载器(Extension ClassLoader),它是Java实现的,独立于虚拟机,主要负责加载<JAVA_HOME>Wib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。
3)应用程序类加载器(Application ClassLoader),它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那这个加载器就是我们程序中的默认加载器。
-------------------------------------------------------------------------------------------------------------
01.Java 中有哪些垃圾回收算法?
标记-清除
复制算法
标记-整理算法
02.Java 中常见的垃圾收集器有哪些?
1.Serial 收集器
2.ParNew 收集器
3.Parallel Scavenge 收集器
4.Serial Old 收集器
5.Paralled Old 收集器
6.CMS 收集器
7.G1 收集器
03.Java中如何判断对象是否是垃圾?不同垃圾回收方法有何区别?
一共有两种方式,分别是引用计数和可达性分析。
引用计数有循环依赖的问题,但是是可以解决的。
可达性分析则是从根引用(GCRoots)开始进行引用链遍历扫描,如果可达则对象存活,如果不可达则对象已成为垃圾。
04.为什么 Java 的垃圾收集器将堆分为老年代和新生代?
因为不同对象的生命周期不一样,大部分对象朝生夕死,而少部分一直存在堆中,所以按照存活时间分区管理更加高效。
也因为不同分区的生命周期不同,所以可以采用不同的清除算法来优化处理,像新生代的对象“死亡率“比较高,因此标记复制比较合适(大部分对象都消失了,把存活的复制到一边,死亡的全部清理即可)
而老年代的对象存活时间比较长,因此标记清除即可(存活对象比较多,整理或复制耗时比较长)
且分区后可以减少GC暂停的时间,你想想每次处理一个堆的数据,还是将堆分区处理来的快?
总而言之,分区是为了更高效地管理不同生命周期的对象。
1.20 设计模式
01.五大原则
1.开闭原则
2.单一职责原则
3.里氏替换原则
4.接口隔离原则
5.依赖倒置原则
02.23种设计模式
a.创建型模式:
1.单例模式:饿汉式、懒汉式、双重检查锁、静态内部类、枚举方法、带volatile的双重校验锁法、使用ThreadLocal实现单例模式、使用CAS锁实现
2.工厂方法模式
3.抽像工厂模式
4.建造者模式
5.原型模式
b.结构性模式
1.享元模式
2.外观模式
3.适配器模式
4.装饰者模式
5.组合模式
c.行为型模式
1.策略模式
2.模板方法模式
3.观察者模式
4.责任链模式
5.命令模式
6.备忘录模式
7.迭代器模式
8.状态模式
03.Spring使用的设计模式
a.简单工厂
Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
b.工厂方法
实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean的getOjbect()方法的返回值。
c.单例模式
Spring依赖注入Bean实例默认是单例的。Spring的依赖注入(包括lazy-init方式)都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。
d.适配器模式
SpringMVC中的适配器HandlerAdatper,它会根据Handler规则执行不同的Handler。即DispatcherServlet根据HandlerMapping返回的handler,向HandlerAdatper发起请求处理Handler。HandlerAdapter根据规则找到对应的Handler并让其执行,执行完毕后Handler会向HandlerAdapter返回一个ModelAndView,最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。
e.装饰器模式
Spring中用到的装饰器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。
f.代理模式
AOP底层就是动态代理模式的实现。即:切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。
g.观察者模式
Spring的事件驱动模型使用的是观察者模式,Spring中Observer模式常用的地方是listener的实现。
h.策略模式
Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。Resource 接口是具体资源访问策略的抽象,也是所有资源访问类所实现的接口。
i.模板方法模式
Spring模板方法模式的实质,是模板方法模式和回调模式的结合,是Template Method不需要继承的另一种实现方式。Spring几乎所有的外接扩展都采用这种模式。
04.Spring使用的设计模式
①工厂模式:创建bean,获取bean
②单例模式/原型模式:创建bean,设置作用域
③监听模式:自定义事件发布,例如ApplicationListener,当某个动作触发时,会自动执行一个通知
④责任链模式:AOP
⑤策略模式:创建代理
05.SpringMVC使用的设计模式
①工厂模式(Factory):HandlerMapping和ViewResolver:使用工厂模式创建相应的处理器和视图对象。
②策略模式(Strategy):HandlerMapping和HandlerAdapter:定义了请求处理的策略,具体的实现类如RequestMappingHandlerMapping和SimpleControllerHandlerAdapter。
③代理模式(Proxy):AOP(面向切面编程):通过代理在控制器方法调用前后添加额外的处理逻辑,如事务管理和安全检查。
④模板方法模式(Template Method):AbstractController:定义了处理请求的模板方法,具体的处理逻辑由子类实现。
⑤单例模式(Singleton):Spring Bean:默认是单例模式,确保每个控制器在Spring容器中只有一个实例。
06.SpringBoot使用的设计模式
①单例模式(Singleton):Spring容器管理的Bean默认是单例模式,确保每个Bean在容器中只有一个实例。
②工厂模式(Factory):BeanFactory和ApplicationContext都是工厂模式的实现,用于创建和管理Bean实例。
③代理模式(Proxy):AOP(面向切面编程)使用了代理模式,通过动态代理在方法调用前后添加额外的处理逻辑。
④模板方法模式(Template Method):JdbcTemplate和RestTemplate等模板类,定义了操作的骨架,具体的步骤由子类实现。
07.MyBatis使用的设计模式
①代理模式(Proxy):MyBatis使用动态代理为Mapper接口生成实现类,处理数据库操作。
②工厂模式(Factory):SqlSessionFactory用于创建SqlSession对象,管理MyBatis的会话。
③模板方法模式(Template Method):BaseExecutor定义了执行查询和更新的骨架,具体实现由子类提供。
④建造者模式(Builder):MyBatis的SqlSessionFactoryBuilder用于构建SqlSessionFactory实例。
2 数组
2.1 数组本质是对象
01.定义
数据类型 长度 内存空间
变量 某个数据类型 一个空间
数组 基本类型,或引用类型(类型兼容) 固定长度 连续空间
容器 包装类,或引用类型(类型兼容) 可变长度 ArrayList“动态数组”实现,连续空间
02.JAVA数组
a.图示
例:String[] array = new String[]{"abc", "def", "ghi"}
栈:数组对象(看成一个指针,指向堆中地址)
堆:数组元素
栈 堆 常量池
【0】 -> "abc"
String[] array = new String[]{"abc", "def", "ghi"} 【1】 -> "def"
【2】 -> "ghi"
b.定义
数组,就是相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,
然后用编号区分他们的变量的集合,这个名字称为数组名,编号称为下标。
-----------------------------------------------------------------------------------------------------
长度:【数组长度一旦定义,则无法再改变】
要素:数组名、下标、类型、数组元素
特点:数组类型与数组元素的类型一致(或兼容)的集合,并使用【变量进行命名】,【下标进行区分】,效率高于容器
一旦数组完成初始化,数组在内存中所占的空间将被固定下来,所以数组的长度将不可改变
不要静态初始化和动态初始化同时使用,千万不要数组初始化时,既指定数组长度,也为每个数组元素分配初始值
c.本质是对象,是一种引用类型
a.介绍
对象的基本特点:封装数据、访问属性、调用方法,所以数组是对象
若两个类A和B,且B继承了A,那么【A[]类型的引用,就指向B[]类型的对象】
举例1:
int[] b = {0, 1, 2, 3, 4};
int[] c;
c = b;
-------------------------------------------------------------------------------------------------
在Java中,可以将一个数组的引用赋值给另外一个数组;经过c=b的操作,数组c的引用同样指向了b
b.JAVA数组 VS C++数组
int[] a = new int[4];
a.clone();
a.toString();
数组可以调用Object中的方法,所以数组的最顶层父类也是Object
Java中的数组具有对象的一些基本特点:封装了一些数据,可以访问属性,也可以调用方法,所以数组是对象。
-------------------------------------------------------------------------------------------------
int main(){
int a[] = {1, 2, 3, 4};
int* pa = a; // 无法访问属性,也不能调用方法。
return 0;
}
而C++数组虽然封装了数据,但数组名只是一个指针,指向数组中的首个元素,既没有属性,也没有方法可以调用
因此所以C++中的数组不是对象,只是一个数据的集合,而不能当做对象来使用
d.数组的类型
a.Class类
int[] a1 = {1, 2, 3, 4};
System.out.println(a1.getClass().getName()); // [I
String[] s = new String[2];
System.out.println(s.getClass().getName()); // [Ljava.lang.String;
String[][] ss = new String[2][3];
System.out.println(ss.getClass().getName()); // [[Ljava.lang.String;
-------------------------------------------------------------------------------------------------
既然是对象,那么就必须属于一个类型,比如根据Person类创建一个对象,这个对象的类型就是Person。
所以,数组也是有类型的。只是这个类型显得比较奇怪。
你可以说a1的类型是int[],这也无可厚非,但是我们没有自己创建这个类,也没有在Java的标准库中找到这个类。
这只能有一个解释,那就是虚拟机自动创建了数组类型,可以把数组类型和8种基本数据类型一样,
b.命名规则
当做java的内建类型,这种类型的命名规则是这样的:
每一维度用一个[表示;开头两个[,就代表是二维数组
[后面是数组中元素的类型,包括基本数据类型和引用数据类型
-------------------------------------------------------------------------------------------------
使用层面,String[] s = new String[2]; 类型是String[]
JVM层面,String[] s = new String[2]; 类型为[java.lang.String,普通的类在JVM类型为【包名+类名】
e.数组的继承
a.Class类
String[] s = new String[5];
// 成立,说明可以用Object[]的引用来接收String[]的对象
Object[] obja = s;
// 打印结果为java.lang.Object,说明String[] 的直接父类是 Object而不是Object[]
System.out.println(s.getClass().getSuperclass().getName());
b.不是严格意义上的继承
由代码可知,String[]的直接父类就是Object而不是Object[]。
可是Object[]的引用明明可以指向String[]类型的对象,那么他们的继承关系有点像这样:
String[] ---------------------> Object[] ---------------------> Object
↓ ↑
-----------------------------------------------------------------
这样的话就违背了Java单继承的原则。String[]不可能即继承Object,又继承Object[]。上面的类图肯定是错误。
那么只能这样解释:数组类直接继承了Object,关于Object[]类型的引用能够指向String[]类型的对象,
这种情况只能是Java语法之中的一个特例,并不是严格意义上的继承。
也就是说,String[]不继承自Object[],但是我可以允许你向上转型到Object[],这种特性是赋予你的一项特权。
c.总结
如果有两个类A和B,如果B继承(extends)了A,那么A[]类型的引用就可以指向B[]类型的对象。
public static class Father {
}
public static class Son extends Father {
}
Son[] sons = new Son[3];
//成立
Father[] fa = sons;
// java.lang.Object,说明Son[]的直接父类是Object
System.out.println(sons.getClass().getSuperclass().getName());
-------------------------------------------------------------------------------------------------
Son[][] sonss = new Son[2][4];
Father[][] fathers = sonss;
根据上面的结论,Father[][]的引用可以指向Son[][]类型的对象。
-------------------------------------------------------------------------------------------------
int[] aa = new int[4];
Object[] objaa = aa; //错误的,不能通过编译
因为int不是引用类型,Object不是int的父类,在这里自动装箱不起作用
但是,可以通过这种方式是可以的,Object[] objss = {"aaa", 1, 2.5};
这种情况下自动装箱可以工作,也就是说,Object数组中可以存放任何值,包括基本数据类型。
03.JAVA数组
a.变量与数组
变量:内存中的【一个单独】空间,如a,b,c
数组:内存中的【一个连续】空间,如score[1], score[2], score[3] ... score[9]
b.为什么使用数组?简化
int score1 = 88;
int score2 = 88;
int score3 = 88;
...
int score9 = 88;
c.什么时候使用数组?
数组类型与数组元素的类型一致(或兼容)的集合,并使用【变量进行命名】,【下标进行区分】,效率高于容器
如,Person[] people = new Person[]{new Person("xujian", 10), new Person("xiewei", 20),};
如,long[] nums = new long[3]; nums[0] = 70; nums[1] = 80; nums[2] = 90L;
如,long[] nums = new long[]{70, 80, 90L}; // byte,short,char —> int —> long —> float —> double
2.2 C++数组只是数据集合
01.存储结构
a.C/C++数组
数组空间是一次性给定的,优先访问低地址,自底向上而放元素。
在内存中是连续存储的,并且所有数组都是连续的,都可作为一维数组看待。
同时,C数组可以动态申请内存空间,可以动态扩容的,而Java数组不行,当然Java也提供了ArrayList动态数组类
如图,一个二维数组就可以看成一个一维数组,只是里面存放的元素为一维数组。所以C中的数组是呈线性结构
b.JAVA数组
Java中的数组就不一样,在Java中,数组都是引用实体变量,呈树形结构,
每一个叶子节点之间毫无关系,只有引用关系,每一个引用变量只引用一个实体。
Java数组是会做边界检查的,所以当你越界访问时,会抛出 RuntimeException,而在C或C++是不做边界检查的
如图,在堆内存中,各个一维数组的元素是连续的,但各个一维数组之间不是连续存放的。
02.JAVA数组
a.数据放在别的地方,容器存放内存区域的一个标识
Java中,容器是用来保存多个对象的东西,严格来说是保存对象的引用。
因为对象实际的数据是放在另外的地方的,放在容器中的只是指向那块内存区域的一个标识
b.数组与容器的区别
Java中,既然有了强大的容器,是不是就不需要数组了?答案是不
诚然,大多数情况下,应该选择容器存储数据。
-----------------------------------------------------------------------------------------------------
数组和容器的区别有:效率、类型识别、以及存放基本类型的能力:
①Java中,数组是一种效率最高的存储和随机访问对象引用序列的方式,数组的效率要高于容器(如 ArrayList)
②类型识别方面,Java容器List、Set和Map在处理对象的时候就好像这些对象都没有自己的类型一样,
容器将它所含的元素都看根类Object类型,这样我们只需创建一种容器,就能把所有的类型的对象全部放进去。
但是当取出数据时,需要我们自己进行类型转换,这个问题在Java引入泛型进行类型检查后,与容器类一起使用
就可以解决类型转换的问题
③数组可以持有基础类型,而容器则不能(必须用到包装类)
c.随机访问
非随机访问:就是存取第N个数据时,必须先访问前(N-1)个数据 (链表)
随机访问:就是存取第N个数据时,不需要访问前(N-1)个数据,直接就可以对第N个数据操作(数组)
-----------------------------------------------------------------------------------------------------
数组是如何做到随机访问的?
事实上,数组的数据是按顺序存储在内存的连续空间内的,从图种我们看出来,即便Java二维数组是呈树形结构,
但是各个一维数组的元素是连续的,通过arr[0],arr[1]等数组对象指向一维数组,所以每个数据的内存地址
(在内存上的位置)都可以通过数组下标算出,我们也就可以借此直接访问目标数据,也就是随机访问
d.Java数组与内存
如图,数组对象(类比看作指针)存储在栈中,数组元素存储在堆中
一维数组在堆上连续的内存空间直接存储值,二维数组在连续的地址上存储一维数组的引用地址,一维数组与一维数组
并不一定靠在一起,但是这些一维数组内部的值是在连续地址上的。更高维的数组继续以此类推,只有最后一维数组在
连续地址上保存值,其他纬度均在连续地址上保存下一维度的引用地址,同维度的实例不一定靠在一起
e.数组下标为什么是从0开始?
前面说到数组访问数据时使用的是随机访问(通过下标可计算出内存地址),从数组存储的内存模型上来看,“下标”最
确切的定义应该是“偏移(offset)”。如果用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,
a[k] 就表示偏移 k 个 type_size 的位置,所以计算 a[k] 的内存地址只需要用这个公式:
a[k]_address = base_address + k * type_size
但是,如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:
a[k]_address = base_address + (k-1)*type_size
对比两个公式,可以发现,从0开始编号,每次随机访问数组元素都少了一次减法运算,
对于CPU来说,就是少了一次减法指令,提高了访问的效率
03.JAVA数组
a.对象定义
在较高的层面上,对象是根据某个类创建出来的一个实例,表示某类事物中一个具体的个体。
对象具有各种属性,并且具有一些特定的行为。
而在较低的层面上,站在计算机的角度,对象就是内存中的一个内存块,在这个内存块封装了一些数据,
也就是类中定义的各个属性,所以,对象是用来封装数据的。
b.Java中的数组是对象吗?
全凭Java的设计者决定。
在较高的层面上,数组不是某类事物中的一个具体的个体,而是多个个体的集合。那么它应该不是对象。
而在计算机的角度,数组也是一个内存块,也封装了一些数据,这样的话也可以称之为对象。
c.数组到底是不是对象,通过代码验证:
int[] a = new int[4];
a.clone();
a.toString();
数组可以调用Object中的方法,所以数组的最顶层父类也是Object
Java中的数组具有对象的一些基本特点:封装了一些数据,可以访问属性,也可以调用方法,所以数组是对象。
从上面的代码来看,在数组arr上, 可以访问它的属性,也可以调用一些方法。这基本上可以认定,Java中的数组
也是对象,它具有java中其他对象的一些基本特点:封装了一些数据,可以访问属性,也可以调用方法。
所以答案是肯定的,数组是对象。
Java 数组在内存中的存储:
- 数组对象(这里可以看成一个指针)存储在栈中,数组元素存储在堆中;
- 只有当 JVM 执行 new String[]时,才会在堆中开辟相应的内存区域;
- 数组对象 array 可以视为一个指针,指向这块内存的存储地址;
一个二维数组就可以看成一个一维数组,只是里面存放的元素为一维数组。所以C中的数组是呈线性结构
在堆内存中,各个一维数组的元素是连续的,但各个一维数组之间不是连续存放的。
一维数组,数组对象(类比看作指针)存储在栈中,数组元素存储在堆中
二维数组,数组对象(类比看作指针)存储在栈中,数组元素存储在堆中
一个Person对象在内存中的表示:
- 红色矩形表示一个引用或一个基本类型的数据,绿色矩形表示一个对象,多个红色矩形组合在一块,可组成一个对象
- name在对象中只表示一个引用, 也就是一个地址值,它指向一个真实存在的字符串对象。在这里严格区分了引用和对象
一个数组在内存中的表示:数组是否为对象,全凭Java的设计者决定
2.3 一维数组、多维数组、泛型数组
01.一维数组
a.声明数组
a.正确
int[] nums; // 声明数组(推荐)
int nums[]; // 声明数组
-------------------------------------------------------------------------------------------------
float[] nums; // 声明数组(推荐)
float nums[]; // 声明数组
-------------------------------------------------------------------------------------------------
boolean[] nums; // 声明数组(推荐)
boolean nums[]; // 声明数组
-------------------------------------------------------------------------------------------------
char[] nums; // 声明数组(推荐)
char nums[]; // 声明数组
-------------------------------------------------------------------------------------------------
String[] nums; // 声明数组(推荐)
String nums[]; // 声明数组
b.错误
int[5] intErrorArray; // 错误 ×
int intErrorArray[5]; // 错误 ×
-------------------------------------------------------------------------------------------------
左侧[]:声明数组时不能指定其长度(数组中元素的个数),因为数组是一种引用类型的变量,
因此它定义一个变量时,仅仅表示定义了一个引用变量(指针),还未指向任何有效的内存,
还没有内存空间来存储数组元素,所以不能指定数组的长度,只有在数组进行初始化后才可以使用。
只有在数组真正创建时才会分配空间,因此,编译器不允许在此指定数组的大小。
b.创建数组
a.声明+创建
int[] intArray; // 声明
float[] floatArray; // 声明
boolean[] boolArray; // 声明
char[] charArray; // 声明
String[] stringArray; // 声明
-------------------------------------------------------------------------------------------------
intArray = new int[3]; // √,创建同时,不初始化数组则必须指定大小
intArray = new int[]; // ×,创建同时,不指定大小,则必须初始化
intArray = new int[]{0,1,2}; // √,创建同时,不指定大小,则必须初始化
floatArray = new float[3]; // 创建
boolArray = new boolean[3]; // 创建
charArray = new char[3]; // 创建
stringArray = new String[3]; // 创建
-------------------------------------------------------------------------------------------------
System.out.println(intArray[0]); // 默认值:0, 整数类型(byte、short、int、long)
System.out.println(floatArray[0]); // 默认值:0.0 浮点类型(float、double)
System.out.println(boolArray[0]); // 默认值:false 布尔类型(boolean)
System.out.println(charArray[0]); // 默认值:'\u0000' 字符类型(char)
System.out.println(stringArray[0]); // 默认值:null 引用类型(类、接口、数组、String)
b.分析
右侧[]:一旦使用NEW为数组分配内存空间,也就是数组元素有初始值,即使存储内容是空,这个空也是一个值null
也就是说,不可能只分配内容空间而不赋初始值,即使没有指定初始值,系统也会自动为其分配;
如,基础数据类型的包装类,默认初始化值为null,
基础数据类型的包装类创建的数组属于引用数组,引用数组默认初始化值为null
-------------------------------------------------------------------------------------------------
一旦数组完成初始化,数组在内存中所占的空间将被固定下来,所以数组的长度将不可改变
不要静态初始化和动态初始化同时使用,千万不要数组初始化时,既指定数组长度,也为每个数组元素分配初始值
c.初始化数组
a.分类1:数组的初始化分为静态初始化、动态初始化、默认初始化
a.静态初始化
不声明个数(数组维度),只赋值
int[] intArray = new int[]{20,21,22}; √
或 int[] intArray = {30,31,32}; √
int[] intError = new int[3]{50,51,52}; ×,静态初始化不能指定元素个数
b.动态初始化
只声明个数(数组维度),但不赋值
int[] intArray = new int[3]; √
intArray[0] = 1;
intArray[1] = 2;
intArray[2] = 3;
int[] intError = new int[]; ×,动态初始化必须指定元素个数
c.默认初始化
int[] intArray = new int[3]; √
b.分类2:数组的初始化分为指定数组维度、不指定数组维度
a.指定数组维度
为数组开辟指定大小的数组维度。
如果数组元素是基础数据类型,会将每个元素设为默认值;如果是引用类型,元素值为null
b.不指定数组维度
用花括号中的实际元素初始化数组,数组大小与元素数相同
c.示例1
int[] num1 = new int[3]; // 形式1:只声明个数(数组维度),但不赋值
int[] num2 = new int[]{97, 98, 99}; // 形式2:不声明个数(数组维度),只赋值
int[] num3 = {97,98,99}; // 形式3:不声明个数(数组维度),只赋值(简化)
System.out.println(num1.length); | 3 // 声明个数后,无论是否赋值,都会开辟内存空间
for (int item : num1) { | 0
System.out.println(item); | 0 // 未赋值,基础数据类型(默认值)
} | 0
System.out.println(num2.length); | 3
for (int item : num2) { | 97 // 赋值
System.out.println(item); | 98 // 赋值
} | 99 // 赋值
d.示例2
User[] array1 = new User[3];
User[] array2 = new User[]{new User(), new User(), new User()};
System.out.println(array1.length); | 3 // 声明个数后,无论是否赋值,都会开辟内存空间
for (User item : array1) { | null
System.out.println(item); | null // 未赋值,引用数据类型(null)
} | null
System.out.println(array2.length); | 3
for (User item : array2) { | org.myslayers.array.ArrayDemo2$User@4141d797
System.out.println(item); | org.myslayers.array.ArrayDemo2$User@68f7aae2
} | org.myslayers.array.ArrayDemo2$User@9845sfs2
d.数组的维度
a.创建维度
维度形式:整数、字符、整数型、字符型变量、计算结果为整数或字符的表达式
维度大小:数组维度并非没有上限的,如果数值过大,编译时会报错
b.示例
int length = 3;
int[] array1 = new int[4.0]; | 报错
int[] array2 = new int["test"]; | 报错
int[] array3 = new int['a']; | 数组维度:字符 只要转换为整数即可
int[] array4 = new int[length]; | 数组维度:变量 只要转换为整数即可
int[] array5 = new int[length + 2]; | 数组维度:表达式 只要转换为整数即可
int[] array6 = new int['a' + 2]; | 数组维度:表达式 只要转换为整数即可
int[] array7 = new int[6553612431]; | 数组维度过大,编译报错
e.数组的访问
a.访问
Java 中,可以通过在 [] 中指定下标,访问数组元素,下标位置从0开始
b.示例
int[] array = {1, 2, 3};
for (int i = 0; i < array.length; i++) {
array[i]++;
System.out.println(String.format("array[%d] = %d", i, array[i]));
}
-------------------------------------------------------------------------------------------------
array[0] = 2
array[1] = 3
array[2] = 4
f.数组的长度
a.一维数组
int[] s = new int[4];
System.out.println(s.length); // 一维数组长度:4
b.二维数组
String[][] s = new String[2][];
s[0] = new String[2];
s[1] = new String[3];
s[0][0] = new String("Good");
s[0][1] = new String("Luck");
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");
System.out.println(s.length); // 二维数组的【整体长度】:2
System.out.println(s[0].length); // 二维数组的【第1个,一维数组长度】:2
System.out.println(s[1].length); // 二维数组的【第2个,一维数组长度】:3
c.区别
a.针对数组的length属性
int[] array = {1, 2, 3};
System.out.println(array.length); // 数组int[]的长度
String[] list = {"A", "B", "C"};
System.out.println(list.length); // 数组String[]的长度
b.针对字符串String的length()方法
String a = "abc";
System.out.println(a.length()); // 字符串String的长度
c.针对集合的size的size()方法
List<Object> array=new ArrayList();
array.add("a");
array.add("b");
array.add("c");
System.out.println(array.size()); // 泛型List的元素个数
g.数组与参数
a.数组作为函数的参数 - 右边
public class Demo {
private static void fun(int[] array) { // 参数:int[]
for (int i : array) {
System.out.print(i + "\t");
}
}
public static void main(String[] args) {
int[] array = new int[]{1, 3, 5};
fun(array); // 调用fun()
}
}
-------------------------------------------------------------------------------------------------
1
3
5
b.数组作为函数的返回值 - 左边
public class Demo {
private static int[] fun() { // 返回值:int[]
return new int[] {1, 3, 5};
}
public static void main(String[] args) {
int[] array = fun(); // 调用fun()
System.out.println(Arrays.toString(array));
}
}
-------------------------------------------------------------------------------------------------
[1, 3, 5]
h.数组的应用场景
a.数组的缺点
数组在内存中是连续存储的,所以索引速度是非常的快,数组的赋值与修改元素也很简单
但是数组也有不足的地方,那就是如果想在两个相邻元素之间插入新的元素会非常麻烦
另外,数组声明的时候必须指定数组的长度,而数组的长度是不可变的
因此,数组长度过长会造成内存浪费,长度过短则会造成溢出
b.数组的优点
数据比较少,能够确定长度;存取或修改操作较多,插入和删除较少的情况
遍历时,经常需要按照序号来进行访问数据元素或做运算的情况
对性能要求较高时,数组是首选,特别是针对基础类型进行操作,效率提升甚至可以达到基于List等集合性能的10倍
c.遍历Array求和 VS 遍历List求和
// 对数组求和
public static int sum(int[] datas) {
int sum = 0;
for (int data : datas) {
sum += data;
}
return sum;
}
// 对List求和
public static int sum(List<Integer> datas) {
int sum = 0;
for (Integer data : datas) {
sum += data; // 拆箱操作
}
return sum;
}
-------------------------------------------------------------------------------------------------
在上述两个方法中,影响性能的最大地方便是List中的Integer对象的拆箱和装箱操作,特别是数据量比较大的时候。
基础类型是在栈内存中操作的,栈内存的特点是速度快、容量小;
对象类型是在堆内存中操作的,堆内存的特点是速度慢、容量大;
因此从性能上来讲,基本类型的处理占优势;
-------------------------------------------------------------------------------------------------
有人可能会说有整型缓存池的存在。但整型缓存池容纳的是﹣128到127之间的Integer对象,
超过这个范围,便需要创建Integer对象了,而超过这个容纳范围基本上是大概率事件。
02.多维数组
a.定义
Java 可以支持二维数组、三维数组、四维数组、五维数组
二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组。
b.二维数组
String[][] str = new String[3][4]; // 形式1:只声明个数(数组维度),但不赋值
------------------------------------------------------------
String[][] s = new String[2][]; // 形式1:只声明个数(数组维度),但不赋值
s[0] = new String[2]; // 第1行,指定2个元素
s[1] = new String[3]; // 第4行,指定3个元素
s[0][0] = new String("Good");
s[0][1] = new String("Luck");
s[0][2] = new String("数组越界"); // 报错:ArrayIndexOutOfBoundsException
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");
-----------------------------------------------------------------------------------------------------
Integer[][] a1 = { // 形式2:不声明个数(数组维度),只赋值
{1, 2, 3}, // 自动装箱
{4, 5, 6},
};
System.out.println("a1: " + Arrays.deepToString(a1));
-----------------------------------------------------------------------------------------------------
String[][] a2 = { // 形式2:不声明个数(数组维度),只赋值
{"The", "Quick", "Sly", "Fox"},
{"Jumped", "Over"},
{"The", "Lazy", "Brown", "Dog", "and", "friend"},
};
System.out.println("a2: " + Arrays.deepToString(a2));
c.三维数组
Double[][][] a3 = { // 形式2:不声明个数(数组维度),只赋值
{ {1.1, 2.2}, {3.3, 4.4} },
{ {5.5, 6.6}, {7.7, 8.8} },
{ {9.9, 1.2}, {2.3, 3.4} },
};
System.out.println("a3: " + Arrays.deepToString(a3));
03.泛型数组:存储泛型,建议使用“容器”,而不是“数组”(Java中不允许直接创建泛型数组)
a.代码
public class Demo {
static class GenericArray<T> {
private T[] array;
public GenericArray(int num) {
array = (T[]) new Object[num];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) { return array[index]; }
public T[] array() { return array; }
}
public static void main(String[] args) {
GenericArray<Integer> genericArray = new GenericArray<Integer>(4);
genericArray.put(0, 0);
genericArray.put(1, 1);
Object[] array = genericArray.array();
System.out.println(Arrays.deepToString(array));
}
}
b.分析
Peel<Banana>[] peels = new Pell<Banana>[10]; // 这行代码非法
Java 中不允许直接创建泛型数组。但是可以通过创建一个类型擦除的数组,然后转型的方式来创建泛型数组
2.4 遍历数组和集合
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。
迭代器通常被称为“轻量级”对象,因为创建它的代价小。
Java中的Iterator功能比较简单,并且只能单向移动:
(1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。
(2) 使用next()获得序列中的下一个元素。
(3) 使用hasNext()检查序列中是否还有元素。
(4) 使用remove()将迭代器新返回的元素删除。
01.遍历数组和集合,一般有以下三种形式
a.普通for循环
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + ",");
}
b.增强for循环,也叫foreach
for (Integer i : list) {
System.out.print(i + ",");
}
c.迭代器遍历
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + ",");
}
02.方式一:普通for循环
a.数组
int[] nums = new int[]{1, 2, 3, 4}
for (int i=0; i<num.length; i++) { for (初始值; 循环条件; 更新变量) {
System.out.println(num[i]); 循环操作
} }
b.集合
a.遍历ArrayList
List<String> arr = new ArrayList<String>();
arr.add("元素1");
arr.add("元素2");
arr.add("元素3");
for (int i = 0; i < arr.size(); i++) { // for适用于循环次数已知
System.out.println(arr.get(i));
}
-------------------------------------------------------------------------------------------------
元素1
元素2
元素3
b.遍历HashMap
// 创建一个HashMap
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("One", 1);
hashMap.put("Two", 2);
hashMap.put("Three", 3);
// 获取键的集合
Object[] keys = hashMap.keySet().toArray();
// 使用普通for循环遍历键,并获取对应的值
for (int i = 0; i < keys.length; i++) {
String key = (String) keys[i];
Integer value = hashMap.get(key);
System.out.println("Key: " + key + ", Value: " + value);
}
-------------------------------------------------------------------------------------------------
Key: One, Value: 1
Key: Two, Value: 2
Key: Three, Value: 3
03.方式二:增强For循环,也叫foreach
a.数组
int[] nums = new int[]{1, 2, 3, 4} // JAVA1.5引入增加型for循环,变量值相当于num[i]
for (int i: nums) { for (元素类型 变量值: 数组/集合){
System.out.println(i) 循环操作
} }
b.集合
a.遍历ArrayList
List<String> arr = new ArrayList<String>(); // foreach适用于循环次数未知
arr.add("元素1");
arr.add("元素2");
arr.add("元素3");
for(String str : arr) {
System.out.println(str);
}
-------------------------------------------------------------------------------------------------
元素1
元素2
元素3
b.遍历ArrayList
List<String> arr = new ArrayList<String>(); // foreach适用于循环次数未知
arr.add("元素1");
arr.add("元素2");
arr.add("元素3");
for (String str : arr) {
arr.add("1"); // 一边迭代一边删除,报错
}
-------------------------------------------------------------------------------------------------
报错:java.util.ConcurrentModificationException,并发修改异常
原因:当迭代器运行的时候,在当前线程A中,会单独的创建一个线程B。
A负责继续迭代,B线程负责删除。B线程每次都会去检查A线程中的元素是否相同,如果不是就会报错。
c.遍历HashMap
Map<String, String> mapstr = new HashMap<String, String>();
mapstr.put("王", "男");
mapstr.put("李", "男");
mapstr.put("张", "女");
for (Map.Entry<String, String> s : mapstr.entrySet()) { // Map.Entry<String, String>指定键值对类型
System.out.print("key=" + s.getKey() + '\t');
System.out.print("value=" + s.getValue() + '\n');
}
-------------------------------------------------------------------------------------------------
key=张 value=女
key=王 value=男
key=李 value=男
c.反编译
a.反编译 - 数组
int [] array = {1,2,3};
for(int i : array){
System.out.println(i);
}
-------------------------------------------------------------------------------------------------
int array[] = {1,2,3};
int [] array$ = array;
for(int len$ = array$.length, i$ = 0; i$<len$; ++i$ ) {
int i = array$[i$]; {
System.out.println(i);
}
}
不难发现,对于数组,foreach循环实际上还是用的【普通for循环】
b.反编译 - ArrayList
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
for(Object obj : list){
System.out.println(obj);
}
-------------------------------------------------------------------------------------------------
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
for(java.util.Iterator i$ = list.iterator(); i$.hasNext();) {
String s = (String) i$.next(); {
System.out.println(s);
}
}
不难发现,对于集合,foreach循环实际上用的是【iterator迭代器迭代】
c.总结
foreach适用于只是进行集合或数组遍历,而for则在较复杂的循环中效率更高;
foreach不能对数组或集合进行修改(添加/删除),如果想要修改就要用for循环;
因此,二者比较下来,for循环更为灵活
03.方式三:迭代器遍历
a.数组
int[] nums = new int[]{1, 2, 3, 4};
Iterator iter = nums.iterator(); // 提示报错,int[]没有一个叫做iterator的方法
b.集合
a.遍历ArrayList
List list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator iter = list.iterator();
while (iter.hasNext()) {
int j = (int) iter.next();
System.out.print(j + "\t");
}
-------------------------------------------------------------------------------------------------
1 2 3
b.遍历HashSet
Set set = new HashSet<>();
set.add(new Theme(1, "标题1", "简介1"));
set.add(new Theme(2, "标题2", "简介1"));
Iterator iter = set.iterator();
while (iter.hasNext()) {
Theme theme = (Theme) iter.next();
System.out.println(theme.getId() + theme.getTitle() + theme.getRemark());
}
-------------------------------------------------------------------------------------------------
1 标题1 简介1
2 标题2 简介1
c.遍历HashMap
Map map = new HashMap<>();
map.put(1, "a");
map.put(2, "b");
map.put(3, "c");
Set set = map.keySet(); // 所有Key组成一个集合
Iterator iter = set.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
Set set2 = map.keySet(); // 所有Key组成一个集合
System.out.println(set2);
Collection col = map.values(); // 所有Value组成一个集合
System.out.println(col);
-------------------------------------------------------------------------------------------------
1
2
3
[1, 2, 3]
[a, b, c]
d.遍历HashMap
Map map = new HashMap<>();
map.put(1, "a");
map.put(2, "b");
map.put(3, "c");
Set<Map.Entry<Integer, String>> entrySet = map.entrySet(); // entrySet是Key:Value的集合
Iterator<Map.Entry<Integer, String>> iter = entrySet.iterator();
while (iter.hasNext()) {
Map.Entry<Integer, String> entry = iter.next();
System.out.print("Key:" + entry.getKey() + "\t");
System.out.print("Value:" + entry.getValue() + "\n");
}
-------------------------------------------------------------------------------------------------
Key:1 Value:a
Key:2 Value:b
Key:3 Value:c
c.一边迭代,一边删除集合中的元素:Iterator是从main线程复制出的一个独立线程
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
for (Object obj : list) {
list.remove(obj); // 一边迭代一边删除,调用Collection(ArrasyList父类)的remove方法
System.out.println(obj); // 该方法只能从集合中删除元素,不能把迭代器中的元素也删除了。
}
-----------------------------------------------------------------------------------------------------
报错:java.util.ConcurrentModificationException,并发修改异常
原因:当迭代器运行的时候,在当前线程A中,会单独的创建一个线程B。
A负责继续迭代,B线程负责删除。B线程每次都会去检查A线程中的元素是否相同,如果不是就会报错。
-----------------------------------------------------------------------------------------------------
Iterator是工作在一个独立的线程中,并且拥有一个mutex锁。
Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,
这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,
所以按照 fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException异常。
-----------------------------------------------------------------------------------------------------
因为ArrayList.remove()实际调用Collection(ArrasyList父类)的remove方法
而该方法只能从集合中删除元素,不能把迭代器中的元素也删除了。
Iterator 在工作的时候是不允许被迭代的对象被改变的,但可以使用Iterator本身的方法remove()来删除对象,
Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。
-----------------------------------------------------------------------------------------------------
改进后的代码如下:
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
Iterator iter = list.iterator();
while (iter.hasNext()) {
Object obj = iter.next();
if (obj.equals(1)) {
iter.remove(); // 调用ArrayList.iterator().remove()
}
}
3 集合
3.1 Collection
01.Java中的集合类主要由Collection和Map这两个接口派生而出,
其中Collection接口又派生出三个子接口,分别是Set、List、Queue。
所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类,这四个接口将集合分成了四大类
Set代表无序的,元素不可重复的集合;
List代表有序的,元素可以重复的集合;
Queue代表先进先出(FIFO)的队列;
Map代表具有映射关系(key-value)的集合。
其中最常用的实现类有HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。
02.集合相比数组的优点:
①Collection的长度会自动适应,不必人工干预
②Collection可以获取到真实的数据个数size0
①用集合可以方便实现对象的增删改查
03.Map和Set有什么区别?
Set代表无序的,元素不可重复的集合;
Map代表具有映射关系(key-value)的集合,其所有的key是一个Set集合,即key无序且不能重复。
04.List和Set有什么区别?
Set代表无序的,元素不可重复的集合;
List代表有序的,元素可以重复的集合。
05.ArrayList和LinkedList有什么区别?
ArrayList和LinkedList默认情况下是线程不安全的。
---------------------------------------------------------------------------------------------------------
ArrayList的实现是基于数组,LinkedList的实现是基于双向链表;
对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,
而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N);
---------------------------------------------------------------------------------------------------------
对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,
不需要像ArrayList那样重新计算大小或者是更新索引;
---------------------------------------------------------------------------------------------------------
LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,
一个指向前一个元素,一个指向后一个元素。
3.2 ArrayList
01.ArrayList和LinkedList的区别是什么?
ArrayList底层数据机构为数组,LinkedList底层数据结构类似链表。
ArrayList随机查找快O(1), LinkedList随机查找慢O(n)。
ArrayList增加和删除元素慢,LinkedList增加和删除元素速度快。
02.ArrayList的缩容方式是什么?
ArrayList可以将数组长度缩小至当前元素个数,缩容函数trimToSize()方法,通常不会主动调用。
03.ArrayList的默认构造函数会不会初始化数组的容量?
不会,第一次add元素时候,会初始化数组容量为10。
04.ArrayList是如何进行扩容的?
设定扩容后数组长度为原来的1.5倍。
如果新数组长度不能满足容量要求minCapacity,将数组长度扩容至minCapacity大小。
如果数组长度超过Integer.MAX_VALUE - 8,将数组长度扩容至Integer.MAX_VALUE大小。
将元素拷贝至新数组中,将引用指向新数组地址
05.什么时候扩容1.5倍后,仍然不能满足容量要求?
当使用addAll方法时,新数组扩容1.5倍后容量,仍然不能满足容量要求,
会将数组大小直接一次扩容至原数组长度和addAll预添加数组长度之和。
06.ArrayList如何执行向特定index添加元素的add方法?
判断index位置是否合法。
判断数组容量是否满足,不满足进行扩容操作。
将index位置开始,到最后一个位置的所有元素,拷贝到原数组index+1开始的位置。
在index位置插入元素。
07.在ArrayList中如何移除一个元素?
首先根据索引或者删除元素,找到预删除元素位置,将该位置后面的所有元素向前移动一个位置,
并将数组最后一个位置值置为null。
08.ArrayList是线程安全的吗?
ArrayList是线程不安全的。Vector是线程安全的。
10、如何实现线程安全的ArrayList?
操作方法用synchronized修饰,SynchronizedList的实现原理。
在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向,CopyOnWriteArrayList实现远离。
-------------------------------------------------------------------------------------------------------------
01.ArrayList扩容机制
底层实现
ArrayList 底层是基于动态数组实现的。当我们向 ArrayList 中添加元素时,如果当前数组的容量不足以容纳新的元素,就需要进行扩容。
默认容量
当创建一个新的 ArrayList 时,如果没有指定初始容量,默认容量为 10。
扩容机制
每次添加元素时,如果当前数组已经满了,ArrayList 会创建一个新的、更大的数组,并将旧数组中的元素复制到新数组中。新的数组容量通常是旧容量的 1.5 倍。
具体实现
以下是 ArrayList 的扩容机制的主要步骤:
检查容量:在添加元素时,检查当前容量是否足够。如果不足,则需要扩容。
计算新容量:新容量通常是旧容量的 1.5 倍,即 newCapacity = oldCapacity + (oldCapacity >> 1)。这种增长策略可以在时间和空间之间取得平衡。
创建新数组:根据计算的新容量,创建一个新的数组。
复制旧数组:将旧数组中的元素复制到新数组中。
引用新数组:将 ArrayList 的内部数组引用指向新数组。
02.ArrayList和LinkedList有什么区别?
访问元素:ArrayList 访问元素更快,适合频繁随机访问。
LinkedList 访问元素较慢,不适合频繁随机访问。
插入和删除元素:ArrayList 插入和删除较慢,适合不频繁插入和删除的场景。
LinkedList 插入和删除更快,适合频繁插入和删除的场景。
内存占用:ArrayList 使用连续内存,空间利用率高。
LinkedList 由于存储额外的指针,内存开销较大。
扩容:ArrayList 需要动态扩容,扩容是一个耗时操作。
LinkedList 不需要扩容,但由于链表结构,可能会导致更多的内存碎片。
03.ArrayList和LinkedList有什么区别?
List<String> arr = new ArrayList<String>(); // foreach适用于循环次数未知
arr.add("one");
arr.add("two");
arr.add("three");
for(String str : arr) {
System.out.println(str);
}
元素1
元素2
元素3
-------------------------------------------------------------------------------------------------
List<String> arr = new LinkedList<String>(); // foreach适用于循环次数未知
arr.add("one");
arr.add("two");
arr.add("three");
for(String str : arr) {
System.out.println(str);
}
元素1
元素2
元素3
04.如何实现线程安全的ArrayList?
操作方法用synchronized修饰,SynchronizedList的实现原理。
在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向,CopyOnWriteArrayList实现远离。
List<String> list = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(list);
Set<String> set = new HashSet<>();
Set<String> syncSet = Collections.synchronizedSet(set);
-------------------------------------------------------------------------------------------------
我之前书上看到的说法是:Vector是相对线程安全,CopyOnWriteArrayList是绝对线程安全
这种说法其实有些问题,CopyOnWriteArrayList在某些场景下还是会报错的
CopyOnWriteArrayList解决了:1.多线程一边读一边写。2.多线程迭代时修改抛出并发修改异常问题
从上可以看出CopyOnWriteArrayList并不是完全意义上的线程安全,如果涉及到remove操作,一定要谨慎处理。
3.3 HashMap
01.描述一下Map put的过程
首次扩容:
先判断数组是否为空,若数组为空则进行第一次扩容(resize);
计算索引:
通过hash算法,计算键值对在数组中的索引;
插入数据:
如果当前位置元素为空,则直接插入数据;
如果当前位置元素非空,且key已存在,则直接覆盖其value;
如果当前位置元素非空,且key不存在,则将数据链到链表末端;
若链表长度达到8,则将链表转换成红黑树,并将数据插入树中;
再次扩容
如果数组中元素个数(size)超过threshold,则再次进行扩容操作。
02.如何得到一个线程安全的Map?
使用Collections工具类,将线程不安全的Map包装成线程安全的Map;
使用java.util.concurrent包下的Map,如ConcurrentHashMap;
不建议使用Hashtable,虽然Hashtable是线程安全的,但是性能较差。
03.HashMap有什么特点?
HashMap是线程不安全的实现;
HashMap可以使用null作为key或value。
04.介绍一下HashMap底层的实现原理
它基于hash算法,通过put方法和get方法存储和获取对象。
存储对象时,我们将K/V传给put方法时,它调用K的hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。
如果发生碰撞的时候,HashMap通过链表将产生碰撞冲突的元素组织起来。在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
05.HashMap中的循环链表是如何产生的?
在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。
06.HashMap为什么线程不安全?
HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。
-------------------------------------------------------------------------------------------------------------
01.HashMap如何实现线程安全?
直接使用Hashtable类;
直接使用ConcurrentHashMap;
使用Collections将HashMap包装成线程安全的Map。
02.说一说HashMap和HashTable的区别
Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点。
Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发空指针异常,但HashMap可以使用null作为key或value。
03.HashMap和LinkedHashMap有什么区别?
Map<String, String> mapstr = new HashMap<String, String>();
mapstr.put("王", "男");
mapstr.put("李", "男");
mapstr.put("张", "女");
for (Map.Entry<String, String> s : mapstr.entrySet()) { // HashMap,乱序
System.out.print("key=" + s.getKey() + '\t');
System.out.print("value=" + s.getValue() + '\n');
}
key=张 value=女
key=王 value=男
key=李 value=男
-------------------------------------------------------------------------------------------------
Map<String, String> mapstr = new LinkedHashMap<String, String>();
mapstr.put("王", "男");
mapstr.put("李", "男");
mapstr.put("张", "女");
for (Map.Entry<String, String> s : mapstr.entrySet()) { // LinkedHashMap,正序
System.out.print("key=" + s.getKey() + '\t');
System.out.print("value=" + s.getValue() + '\n');
}
key=王 value=男
key=李 value=男
key=张 value=女
04.如何得到一个线程安全的Map?
使用Collections工具类,使用SynchronizedMap,将线程不安全的Map包装成线程安全的Map;
使用java.util.concurrent包下的Map,如ConcurrentHashMap;
不建议使用Hashtable,虽然Hashtable是线程安全的,但是性能较差。
4 泛型
01.泛型的作用
①数据安全
②防止类型转换时出错
list.add(默认返回值是Object类型到)
如果加了Double泛型,则自动变成了Iist.add(必须加入Double类型到),其返回值也必须是Double类型的数据
简言之,以Double泛型为例,
如果不加泛型,则默认操作的是Object类型,
如果加了Double泛型,则默认操作的是Double类型
02.说一说你对泛型的理解
Java集合有个缺点—把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。
Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:
集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
由于把对象“丢进”集合时,集合丢失了对象的状态信息,只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。
03.示例1
public static <T> List<T> extractListFromMap(Map<String, Object> map, String key, Class<T> clazz) {
// 获取 Map 中指定 Key 对应的 Value
Object value = map.get(key);
// 判断 Value 是否为 Collection 类型
if (value instanceof Collection<?>) {
return ((Collection<?>) value).stream()
.map(clazz::cast)
.collect(Collectors.toList());
}
throw new IllegalArgumentException("该 Value 不是 Collection 类型");
}
04.示例2
private <T> int handleBatchOperationSysUser(List<T> list, String sqlOperationName) {
if (ObjectUtil.isEmpty(list)) {
return 0;
}
int count = 0;
final int batchSize = 3000;
for (int i = 0, sizes = list.size(); i < sizes; i += batchSize) {
List<T> batchList = list.subList(i, Math.min(i + batchSize, sizes));
try {
switch (sqlOperationName) {
// SysUser
case "insertSysUserBatch":
if (!(batchList.get(0) instanceof SysUser)) {
throw new IllegalArgumentException("insertSysUserBatch 批量数据类型不匹配,期望 SysUser 类型");
}
count += sysUserMapper.upsertSysUserBatch((List<SysUser>) batchList);
break;
case "updateSysUserBatch":
if (!(batchList.get(0) instanceof SysUser)) {
throw new IllegalArgumentException("updateSysUserBatch 批量数据类型不匹配,期望 SysUser 类型");
}
count += sysUserMapper.upsertSysUserBatch((List<SysUser>) batchList);
break;
case "bulkDeleteSysUser":
if (!(batchList.get(0) instanceof String)) {
throw new IllegalArgumentException("bulkDeleteSysUser 批量数据类型不匹配,期望 String 类型");
}
count += sysUserMapper.bulkDeleteSysUser((List<String>) batchList);
break;
default:
throw new IllegalArgumentException("不支持的操作类型: " + sqlOperationName);
}
} catch (Exception e) {
// 执行数据库,运行过程抛出异常抛出异常
throw new RuntimeException("操作数据库异常:" + sqlOperationName, e);
}
}
return count;
}
5 新特性
5.1 Lambda
01.概念
Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)
函数式接口:标注@Functionallnterface,有且只有一个抽象方法。
@Override:检查“重写父类或实现接口的方法”的正确性 可以不添加
@FunctionalInterface:检查“避免在函数式接口中,意外添加其他的抽象方法”的正确性 可以不添加
02.函数式接口从哪来?
1.JDK自带(很多存在于java.util.function包中)
①有参,无返回值(消费型) Consumer<T>
public class use_Consumer_FormattorName {
public static void formattorPersonMsg(Consumer<String[]> con1, Consumer<String[]> con2) {
// con1.accept(new String[]{ "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" });
// con2.accept(new String[]{ "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" });
// 一句代码搞定
con1.andThen(con2).accept(new String[] { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" });
}
public static void main(String[] args) {
formattorPersonMsg((s1) -> {
for (int i = 0; i < s1.length; i++) {
System.out.print(s1[i].split("\\,")[0] + " ");
}
}, (s2) -> {
System.out.println();
for (int i = 0; i < s2.length; i++) {
System.out.print(s2[i].split("\\,")[1] + " ");
}
});
System.out.println();
printInfo(s->System.out.print(s.split("\\,")[0]),
s->System.out.println(","+s.split("\\,")[1]),datas);
}
// 自身自销 有意思
private static void printInfo(Consumer<String> one, Consumer<String> two, String[] array) {
for (String info : array) { // 这里每次产生 {迪丽热巴。性别:女 } String 数据 逻辑那边顺序处理就行
one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:女。 } }
}
}
}
②无参,有返回值(供给型) Supplier<T>
public class use_Supplier_Max_Value {
private static int getMax(Supplier<Integer> suply) {
return suply.get();
}
public static void main(String[] args) {
Integer [] data=new Integer[] {6,5,4,3,2,1};
int reslut=getMax(()->{
int max=0;
for (int i = 0; i < data.length; i++) {
max=Math.max(max, data[i]);
}
return max;
});
System.out.println(reslut);
}
}
③有参,有返回值(函数型) Function<T>
Main{String str = "赵丽颖,20";
solotion(s->s.split("\\,")[1],s->Integer.parseInt(s),s->s+=100,str);
}
private static void solotion(Function<String, String> o1, Function<String,
Integer> o2, Function<Integer, Integer> o3, String str) {
Integer apply = o1.andThen(o2).andThen(o3).apply(str);
System.out.println(apply);
}
④断言式接口 predicate<T>
public class Use_Predicate {
// 判断字符串是否存在o 即使生产者 又是消费者接口
private static void method_test(Predicate<String> predicate) {
boolean b = predicate.test("OOM SOF");
System.out.println(b);
}
// 判断字符串是否同时存在o h 同时
private static void method_and(Predicate<String> predicate1,Predicate<String> predicate2) {
boolean b = predicate1.and(predicate2).test("OOM SOF");
System.out.println(b);
}
//判断字符串是否一方存在o h
private static void method_or(Predicate<String> predicate1,Predicate<String> predicate2) {
boolean b = predicate1.or(predicate2).test("OOM SOF");
System.out.println(b);
}
// 判断字符串不存在o 为真 相反结果
private static void method_negate(Predicate<String> predicate) {
boolean b = predicate.negate().test("OOM SOF");
System.out.println(b);
}
public static void main(String[] args) {
method_test((s)->s.contains("O"));
method_and(s->s.contains("O"), s->s.contains("h"));
method_or(s->s.contains("O"), s->s.contains("h"));
method_negate(s->s.contains("O"));
}
}
2.自定义函数接口
标注@Functionallnterface,有且只有一个抽象方法。
5.2 方法引用
01.方法引用
方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。
02.在Jva8中年,还允许使用:来引用一个已经存在的方法,其语法如下:类名方法名
注意:只写方法名即可,不需要写括号。
03.具体地讲,一共有以下四种类型的引用:
类型 示例
引用静态方法 ContainingClass::staticMethodName
引用某个对象的实例方法 ContainingObject::instanceMethodName
引用某个类型的任意对象的实例方法 ContainingType::methodName
引用构造方法 ClassName::new
5.3 默认方法
00.Java8使用两个新概念扩展了接口的含义:默认方法和静态方法。
默认方法:默认方法就是一个在接口里面有了一个实现的方法
默认方法使得开发者可以在不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。
01.JDK8前,接口中可以定义变量和方法
public interface MyInterface {
public static final int field1 = 0; // 变量:默认修饰符(public、static、final)
int field2 = 0; // 变量:等价上述写法
public abstract void method1(int a) throws Exception; // 方法:默认修饰符(public、abstract)
void method2(int a) throws Exception; // 方法:等价上述写法
}
02.JDK8后,接口中的默认方法,并提供默认实现,【可以对该默认方法重写】
定义:default
调用:Vehicle.super.print();
作用:新增的默认方法在实现类中直接可用
------------------------------------------------------------------------------------------------------
在Java8之前,在基于抽象的设计中,接口只能有抽象方法,一个接口有一个或多个实现类;
【若接口要增加某个方法,则所有实现类都要新增这个方法的实现,否则就就不满足接口的约束】
【默认接口方法,就是为解决这一问题,实现类可以不用修改继续使用,并且新增的默认方法在实现类中直接可用】
【妥协:维护现有代码的向后兼容性时,静态方法和默认方法是一种很好的折衷,逐步为接口提供附加功能,而不破坏实现类】
------------------------------------------------------------------------------------------------------
接口默认方法,扩展接口而不必担心破坏实现类
接口默认方法,缩小了接口和抽象类之间的差异。
接口默认方法,无需创建基类,由实现类自己选择覆盖哪个默认方法实现。
接口默认方法,增强了Java 8中的Collections API以支持lambda表达式。
接口默认方法,默认方法不能为java.lang.Object中的方法,因为Object是所有类的基类,这种写法将毫无意义
------------------------------------------------------------------------------------------------------
如果子类没有重写父接口默认方法的话,会直接继承父接口默认方法的实现;
如果子类重写父接口默认方法为普通方法,则与普通方法的重写类似;
如果子类(接口或抽象类)重写父接口默认方法为抽象方法,那么所有子类的子类需要实现该方法;
------------------------------------------------------------------------------------------------------
多个默认方法:一个类实现了多个接口,且这些接口有相同的默认方法,需要【重写默认方法,或指定哪个接口的默认方法】
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
}
public interface FourWheeler {
default void print(){
System.out.println("我是一辆四轮车!");
}
}
------------------------------------------------------------------------------------------------------
public class Car implements Vehicle, FourWheeler { // 解决办法1:重写默认方法
default void print(){
System.out.println("我是一辆四轮汽车!");
}
}
public class Car implements Vehicle, FourWheeler { // 解决办法2:指定哪个接口的默认方法
public void print(){
Vehicle.super.print();
}
}
03.JDK8后,接口中的静态方法,并提供默认实现,【无法对该静态方法重写】【仅对接口方法可见,实例对象无法访问】
定义:static关键字
调用:Vehicle.blowHorn();
作用:将与相关方法内聚到接口,提高内聚性,无需创建额外对象
------------------------------------------------------------------------------------------------------
【接口提供一种简单的机制,允许通过将相关的方法内聚在接口中,而不必创建新的对象】
【虽然抽象类,也可以“类似接口,提供静态方法,来提高内举性”,但主要区别在于抽象类可以有构造函数、成员变量和方法】
【推荐:把只和接口相关的静态utility方法放在接口中(提高内聚性),而无需额外创建一些utility类专门处理逻辑】
------------------------------------------------------------------------------------------------------
接口静态方法,接口的一部分,实例对象无法直接访问
接口静态方法,非常适合提供有效的方法,例如null检查,集合排序等
接口静态方法,不允许被实现类覆盖,来提供安全性
------------------------------------------------------------------------------------------------------
public interface MyInterface {
default void log(String str) {
if (!isEmpty(str)) {
System.out.println("接口默认log()方法:" + str);
}
}
static boolean isEmpty(String str) {
System.out.println("对“接口默认log()方法”是否为null进行检查");
return str == null ? true : "".equals(str) ? true : false;
}
}
public class Demo implements MyInterface {
public static void main(String[] args) {
Demo demo = new Demo(); // MyInterface接口中,log()调用“静态isEmpty()”
demo.log("");
demo.log("test");
MyInterface.isEmpty(""); // 实例对象无法方法,但可以使用“MyInterface.isEmpty()”
}
}
------------------------------------------------------------------------------------------------------
对“接口默认log()方法”是否为null进行检查
对“接口默认log()方法”是否为null进行检查
接口默认log()方法:test
5.4 Stream API
00.新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
01.在 Java 8 中,集合接口有两个方法来生成流:
stream():为集合创建串行流
parallelStream():为集合创建并行流
当然,流的来源可以是集合,数组,I/O channel,产生器generator等
02.常见方法
filter filter方法用于通过设置的条件过滤出元素。以下代码片段使用filter方法过滤出空字符串。
limit limit方法用于获取指定数量的流。以下代码片段使用limit方法打印出 10 条数据:
sorted sorted方法用于对流进行排序。以下代码片段使用sorted方法对集合中的数字进行排序:
map map方法用于映射每个元素到对应的结果,以下代码片段使用map输出了元素对应的平方数:
forEach forEach方法用于迭代流中的每个数据。以下代码片段使用forEach输出集合中的数字:
Collectors Collectors类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors可用于返回列表或字符串:
5.5 Optional
01.介绍
a.Optional类
Optional是个容器:它可以保存类型T的值,或者仅仅保存null。
Optional类的引入很好的解决空指针异常,可以不用显式进行空值检测。
Optional类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
b.类方法
static <T> Optional<T> empty()
返回一个空的 Optional实例
boolean equals(Object obj)
指示某个其他对象是否等于此可选项
Optional<T> filter(Predicate<? super T> predicate)
如果一个值存在,并且该值给定的谓词相匹配时,返回一个 Optional描述的值,否则返回一个空的 Optional
<U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper)
如果一个值存在,应用提供的 Optional映射函数给它,返回该结果,否则返回一个空的 Optional
T get()
如果 Optional中有一个值,返回值,否则抛出 NoSuchElementException
int hashCode()
返回当前值的哈希码值(如果有的话),如果没有值,则返回0(零)
void ifPresent(Consumer<? super T> consumer)
如果存在值,则使用该值调用指定的消费者,否则不执行任何操作
boolean isPresent()
返回 true如果存在值,否则为 false
<U> Optional<U> map(Function<? super T,? extends U> mapper)
如果存在一个值,则应用提供的映射函数,如果结果不为空,则返回一个 Optional结果的 Optional
static <T> Optional<T> of(T value)
返回具有 Optional的当前非空值的Optional
static <T> Optional<T> ofNullable(T value)
返回一个 Optional指定值的Optional,如果非空,则返回一个空的 Optional
T orElse(T other)
返回值如果存在,否则返回 other
T orElseGet(Supplier<? extends T> other)
返回值(如果存在),否则调用 other并返回该调用的结果
<X extends Throwable>
T orElseThrow(Supplier<? extends X> exceptionSupplier)
返回包含的值(如果存在),否则抛出由提供的供应商创建的异常
String toString()
返回此可选的非空字符串表示,适用于调试
02.使用
a.创建对象
a.静态方法ofNullable()
Author author = getAuthor();
Optional<Author> authorOptional = Optional.ofNullable(author);
-------------------------------------------------------------------------------------------------
在实际开发中,数据很多是从数据库获取的,Mybatis从3.5版本已经支持Optional,直接把dao方法的返回值类型
定义成Optional类型,MyBastis会自己把数据封装成Optional对象返回,封装的过程也不需要我们自己操作。
b.静态方法of()
Author author = new Author();
Optional<Author> authorOptional = Optional.of(author);
-------------------------------------------------------------------------------------------------
一定要注意,如果使用of的时候传入的参数必须不为null。
如果你确定一个对象不是空的则可以使用Optional的静态方法of来把数据封装成Optional对象。
c.静态方法empty()
Optional.empty()
如果一个方法的返回值类型是Optional类型,而如果我们经判断发现某次计算得到的返回值为null,
这个时候就需要把null封装成Optional对象返回。这时则可以使用Optional的静态方法empty来进行封装。
b.安全消费值
a.介绍
获取到一个Optional对象后,可以使用其ifPresent方法对来消费其中的值。
这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码,这样使用起来就更加安全了。
b.代码
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
authorOptional.ifPresent(author -> System.out.println(author.getName()));
c.安全获取值
a.介绍
如果想获取值自己进行处理可以使用get方法获取,但是不推荐,因为当Optional内部的数据为空的时候会出现异常。
如果我们期望安全的获取值,我们不推荐使用get方法,而是使用Optional提供的以下方法。
b.orElseGet
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
Author author1 = authorOptional.orElseGet(() -> new Author());
-------------------------------------------------------------------------------------------------
获取数据并且设置数据为空时的默认值。
如果数据不为空就能获取到该数据,如果为空则根据你传入的参数来创建对象作为默认值返回。
c.orElseThrow
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
try {
Author author = authorOptional.orElseThrow(
(Supplier<Throwable>) () -> new RuntimeException("author为空")
);
System.out.println(author.getName());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
-------------------------------------------------------------------------------------------------
获取数据,如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建异常抛出。
d.过滤
a.介绍
使用filter方法对数据进行过滤。如果原本是有数据的,但是不符合判断,也会变成一个无数据的Optional对象。
b.代码
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
authorOptional.filter(author -> author.getAge()>100)
.ifPresent(author -> System.out.println(author.getName()));
e.判断
a.介绍
使用isPresent方法进行是否存在数据的判断。如果为空返回值为false,如果不为空,返回值为true。
但是这种方式并不能体现Optional的好处,更推荐使用ifPresent方法。
b.代码
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
if (authorOptional.isPresent()) {
System.out.println(authorOptional.get().getName());
}
f.数据转换
a.介绍
Optional提供map对数据进行转换,并且转换得到的数据也还是被Optional包装好的,保证使用安全。
b.代码
private static void testMap() {
Optional<Author> authorOptional = getAuthorOptional();
Optional<List<Book>> optionalBooks = authorOptional.map(author -> author.getBooks());
optionalBooks.ifPresent(books -> System.out.println(books));
}
5.6 新的日期时间
00.介绍
Java 8引入了新的 Date-Time API(JSR 310) 来改进时间、日期的处理!
在旧版的 Java 中,日期时间 API 存在诸多问题,例如:
非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
设计很差:Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类被定义在java.text包中。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
时区处理麻烦:日期类并不提供国际化,没有时区支持,因此 Java 引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
01.java.time包中的关键类和各自的使用例子
Clock类
Clock类使用时区来返回当前的纳秒时间和日期。
Clock可以替代System.currentTimeMillis()和TimeZone.getDefault(),实例如下:
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
LocalDate、LocalTime 和 LocalDateTime类
LocalDate 仅仅包含ISO-8601日历系统中的日期部分,实例如下:
//获取当前日期
final LocalDate date = LocalDate.now();
//获取指定时钟的日期
final LocalDate dateFromClock = LocalDate.now( clock );
System.out.println( date );
System.out.println( dateFromClock );
LocalTime 仅仅包含该日历系统中的时间部分,
//获取当前时间
final LocalTime time = LocalTime.now();
//获取指定时钟的时间
final LocalTime timeFromClock = LocalTime.now( clock );
System.out.println( time );
System.out.println( timeFromClock );
LocalDateTime
LocalDateTime 类包含了 LocalDate 和 LocalTime 的信息,但是不包含 ISO-8601 日历系统中的时区信息,
5.7 Base64
01.介绍
在 Java 7中,我们经常需要使用第三方库就可以进行 Base64 编码。
在 Java 8中,Base64 编码已经成为 Java 类库的标准,实例如下:
02.案例
public class Tester {
public static void main(String[] args) {
final String text = "Base64 finally in Java 8!";
final String encoded = Base64.getEncoder().encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
final String decoded = new String(Base64.getDecoder().decode( encoded ), StandardCharsets.UTF_8 );
System.out.println( decoded );
}
}
6 多线程
6.1 创建线程
01.进程线程区别
①进程:应用程序的执行实例:有独立的内存空间和系统资源
②线程:将进程可以进一步细分为线程:CPU调度和分派的最小单位
举例,QQ是一个进程,QQ又可以细分多个功能(接收消息、发送消息),每个功能都可以通过一个线程来实现。
一个程序至少一个进程,一个进程至少一个线程。
进程是程序运行的实例。运行一个 Java 程序的实质就是启动一个 java 虚拟机进程。
02.多线程
①优点:多线程可以充分的利用CPU资源,提高效率
②缺点:多线程并不是绝对的可以提高效率
补充:多线程并不是越多越好
原因:每开启一个线程会占用1M左右的内存,因此太多的线程会占用大量的内存资源。
单线程有时候性能也非常不错,例如NodeJs、Netty、NIO都是基于单线程的多路复用技术,性能不比多线程差。
03.创建线程
a.继承Thread类
通过继承 Thread 类,并重写它的 run 方法,我们就可以创建一个线程。
首先定义一个类来继承 Thread 类,重写 run 方法。
然后创建这个子类对象,并调用 start 方法启动线程。
b.实现Runnable接口,建议Runnable,面向接口编程
通过实现 Runnable ,并实现 run 方法,也可以创建一个线程。
首先定义一个类实现 Runnable 接口,并实现 run 方法。
然后创建 Runnable 实现类对象,并把它作为 target 传入 Thread 的构造函数中
最后调用 start 方法启动线程。
用lambda表达式实现Runnable接口的run()方法
c.实现 Callable 接口,并结合 Future 实现
首先定义一个 Callable 的实现类,并实现 call 方法。call 方法是带返回值的。
然后通过 FutureTask 的构造方法,把这个 Callable 实现类传进去。
把 FutureTask 作为 Thread 类的 target ,创建 Thread 线程对象。
通过 FutureTask 的 get 方法获取线程的执行结果。
d.通过线程池创建线程
用 JDK 自带的 Executors 来创建线程池对象。
首先,定一个 Runnable 的实现类,重写 run 方法。
然后创建一个拥有固定线程数的线程池。
最后通过 ExecutorService 对象的 execute 方法传入线程对象。
-----------------------------------------------------------------------------------------------------
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,
有多少资源需要投入。这种不确定性将带来以下若干问题:
-----------------------------------------------------------------------------------------------------
为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。
池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。
-----------------------------------------------------------------------------------------------------
JAVA中创建线程池主要有两类方法,
一类是通过Executors工厂类提供的方法,该类提供了4种不同的线程池可供使用。
另一类是通过ThreadPoolExecutor类进行自定义创建。
04.是否所有的线程都是通过start()启动?不是,main()
强制执行join:强制执行调用join0的线程,阻塞当前正在执行的线程
线程的礼让yild: 礼让仅仅是一种尽可能事件,并不一定会100%执行
05.线程状态
1.创建
2.就绪 只缺CPU资源 一切准备就绪,只等CPU分配
3.运行 不仅有CPU资源,而且有其他资源,此时会发生四种状态
①正常死亡
②缺少某个硬件资源·>阻塞·>归还硬件资源,回到“就绪状态”
③要访问的某个变量/方法被上锁->对象锁池中的阻塞·>放行,回到“就绪状态”
④被调用wait0睡眠->对象等待池的阻塞->notifyl睡醒发现排队->放行,回到“就绪状态”
4.阻塞 不仅缺CPU资源,而且缺其他资源
5.死亡
NEW 初始状态,创建线程,但还未调用 start() 方法
RUNNABLE 可执行状态,“就绪” 和 “运行” 两种状态统称 “运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,线程进行登台状态,进入该状态表示当前线程需要等待其他线程做出通知或中断
TIME_WAITING 超时等待状态,不同于 WAITING ,经过指定时间后可以自行返回
TERMINATED 终止状态,表示线程执行完毕
06.线程阻塞的三种情况
等待阻塞(Object.wait -> 等待队列)
同步阻塞(lock -> 锁池)
其他阻塞(sleep/join)
07.线程死亡的三种方式
正常结束
异常结束
调用stop()
08.线程生命周期:
新建(New)
运行(Runnable)
阻塞(Blocked)
无限期等待(Waiting)
限期等待(Time Waiting)
和结束(Terminated)
09.上下文切换:
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,
当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换
10.线程属性的几个方法
getId 用来得到线程ID
getName和setName 用来得到或者设置线程名称
getPriority和setPriority 用来获取和设置线程优先级
setDaemon和isDaemon 用来设置线程是否成为守护线程和判断线程是否是守护线程
6.2 关键字
01.synchronize锁的作用范围:
synchronize作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象。
synchronize作用于静态方法时,锁住的是Class实例
synchronize作用于一个代码块时,锁住的是所有代码块中配置的对象。
02.sleep和wait的区别?
sleep方法属于Thread类,wait方法属于Object类
sleep方法暂停执行指定的时间,让出CPU给其他线程,但其监控状态依然保持在指定的时间过后又会自动恢复运行状态。
在调用sleep方法的过程中,线程不会释放对象锁,而wait会释放对象锁。
03.start方法和run方法有什么区别?
(1)start方法用于启动线程,真正实现了多线程运行。
在调用了线程的start方法后,线程会在后台执行,无须等待run方法体的代码执行完毕。
(2)通过调用start方法启动一个线程时,此线程处于就绪状态,并没有运行。
(3)run方法也叫做线程体,包含了要执行的线程的逻辑代码,在调用run 方法后,线程就进入运行状态,
开始运行run方法中的代码,在run方法运行结束后,该线程终止,CPU在调度其他的线程。
04.sleep()和wait()的区别
sleep() 属于 Thread 类,wait() 属于 Object 类;
sleep() 导致程序暂停执行指定时间,让出 CPU 给其他线程,但 其监控状态依旧保持,指定时间一过就会自动恢复运行状态;
调用 sleep() 方法,线程不会释放对象锁;但调用 wait() 方法时,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify() 方法后本现场才进入对象锁定池准备获取对象锁进入运行状态;
05.start()和run()的区别
start() 方法用于启动线程,真正实现了多线程运行,无需等待 run() 方法体执行完毕就能直接继续执行下面的代码;
通过调用 Thread 类的 start() 方法来启动一个线程,此时线程处于 就绪状态,并没有运行;
方法 run() 称为线程体,主要包含要执行的线程的内容,线程就进入了 运行状态,开始运行 run() 方法中的代码,run() 方法运行结束,则线程终止,然后 CPU 再调度其他线程;
06.start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,
在这个过程中,会为相应的线程分配需要的资源。
07.run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,
便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
08.yield方法
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程
强制执行join:强制执行调用join0的线程,阻塞当前正在执行的线程
09.join方法
实际上调用join方法是调用了Object的wait方法
线程的礼让yild: 礼让仅仅是一种尽可能事件,并不一定会100%执行
10.wait方法:
wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
11.wait、notify、notifyAll(必须都在synchronized中定义)
多个线程在争夺同一个资源时,为了保证协同工作,可以进行线程通信。
通信主要依赖于3个方法:
①wait【自己】:使当前线程处理等待状态(阻塞),等到“其他线程调用此对象的notify()或notifyAll():方法”
②notify【别人】:唤醒一个正在等待线程;如果有多个线程正在等待,则随机唤醒一个。
③notifyAll【别人】:唤醒全部正在等待的线程
6.3 悲观锁、乐观锁
01.volatile 和 synchronized 的区别
volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile 仅能使用在变量级别;synchronized 则可以使用在 变量、方法、和类级别的
volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以 保证变量的修改可见性和原子性
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
02.悲观锁、乐观锁
方式一:加锁(synchronized、Lock) 悲观锁
synchronized Lock
形式 关键字 接口
加锁 { lock{},多种方式(尝试一段时间等)
解锁 },即方法结束,或出现异常 unlock()
状态 无法判断 可以判断
中断 不支持 支持
对应关系 synchronized Lock{ lock(), unlock() }
Object中的wait() Condition中的await()
Object中的notify / notifyAll Condition中的signalAll()
加锁:消耗系统资源比较大(切换上下文对象,造成CPU调度很频繁)
加锁:是一种悲观策略,线程在访问资源时,总认为会和其他资源造成冲突,因此需要加锁。
方法二:CAS无锁算法 乐观锁
CAS:是一种乐观策略,线程在访问资源时总认为不会和其他资源造成冲突,因此有两种可能:
①假设成功(的确设有和其他线程产生冲突)
OK
②假设失败(实际和其他线程产生了冲突)
再试:重新获取最新的资源,然后再次访问(如果发现要访问的资源不是最新的,则一直尝试:直到拿到最新数据为止)
场景:乐观锁类比SVN/Git
03.悲观锁:读多写少
认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。
因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题。
04.乐观锁:写多读少
正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,
只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,
如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。
05.线程通信:信号量Semaphor
Semaphor编程:默认所有的线程都是阻塞
①有个一许可acquire(),只有获取了许可的线程,才能运行
②井且许可的数量,可以通过persmits属性设置
③release(释放许可
6.4 死锁
01.死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
02.产生死锁的根本原因:
①系统资源不足
②多线程的执行顺序不合理:a.算法问题,解决死锁可以通过算法解决:银行家算法:
b.”产生死锁有四个必要条件“,避免死锁就可以打破四种必要条件。
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
6.5 ThreaLocal
01.ThreadLocal
也叫作线程本地变量,其作用是提供线程内的局部变量,这种变量在线程生命周期内其作用,减少同一个线程内多个方法或组件间一些公共变量的传递的复杂度;
02.特点
Java中每一个线程都有自己的专属本地变量, JDK 中提供的ThreadLocal类,ThreadLocal类主要解决的就是
让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
一个线程持有一个链接,该连接对象可以在不同给的方法之间进行线程传递,线程之间不共享同一个连接
6.6 volatile
01.volatile
保证变量内存可见性,防止局部重排序
02.volatile和synchronized的对比
volatile只能修饰变量,性能好。synchronized可以修饰代码块,方法。
volatile不会出现阻塞,synchronized会出现阻塞
volatile保持数据可见性,不支持原子性,synchronized保证原子性,也间接保证可见性
volatile解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程访问资源的同步性