Java基础
1.1 基础概念与常识
1.1.1 Java语言特点
1.语法简单易懂
2.跨平台
3.支持网络编程
4.支持多线程
5.编译与解释并存
6.面向对象(封装,继承,多态)
7.强大的生态(spring全家桶)
面向对象三大特性:
封装:
隐藏对象的属性和实现细节,仅提供公共访问方式来让外界访问,达到保护和隐藏的目的
继承:
定义:
一个类可以继承父类的所有成员变量和方法(不过私有属性和方法虽然继承了但没有权限访问) 能够提高代码的复用性和可维护性(将各个类中重复的变量和代码抽取出来作为父类,提高复用性,同时再要扩展新的子类也不需要更改原来的代码,只需要继承父类即可)
特点:
Java支持多重继承,不支持多继承
可以使用extends和implements两个关键字实现继承,使用 implements 关键字可以变相的使java具有多继承的特性
多态 :
定义:
同一个行为具有多个不同表现形式或形态的能力。就是同一个接口,使用不同的实例而执行不同操作。
应用:
比如一个接口有多个实现类,同一段程序就可以根据传入的实现类不同执行不同的操作
或者一个父类有多个子类,方法可以根据传入参数(子类对象)的不同执行不同的操作
体现:
父类引用可以指向子类对象,但是子类引用不能指向父类对象
父类类型 变量名=new 子类类型();
使用多态后的父类引用变量调用方法时,会优先调用子类重写后的方法
"编译看左边,运行看右边"(多态执行)
Animal c = new Cat(); 左边是 Animal 类(或接口) 右边是 Cat()类; 在编译的时候编译器不管你右边是什么类,只要左边的Animal类(或接口)能编译通过就不会报错。但是运行的时候就要按照右边的Cat()类实际情况来运行。
多态的转型: 向上转型:
多态本身就是向上转型过的过程,子类独有的方法会调用不到(通过父类调用,父类并不知道,这是多态的缺点)
使用格式:父类类型 变量名=new 子类类型();
意义:减少重复代码,
实例化的时候可以根据不同的需求实例化不同的的对象,实现参数统一化
向下转型:
一个已经向上转型的子类对象可以使用强制类型转换的格式,将父类引用类型转为子类引用各类型
使用格式:子类类型 变量名=(子类类型) 父类类型的变量;
意义:原本向上转型中,只能通过父类引用变量调用父类已有的方法,如果调用的方法被子类重写了,那优先调用子类的方法。 但子类独有的方法无法通过父类引用变量调用(编译看左边,无法过编译) 向下转型后,就可以用子类独有的方法了
1.1.2 JVM vs JDK vs JRE
JVM
运行Java字节码的虚拟机
JVM针对不同系统有特定实现,为了使用同样的字节码文件
由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
JDK和JRE
JDK:Java Development Kit
它是功能齐全的 Java SDK(SDK:软件开发工具包)。它拥有 JRE 所拥有的一切(包含了 JRE),还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译 Java 程序
JRE:Java Runtime Environment Java运行时环境
是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
1.1.3 什么是字节码?采取字节码的好处
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件)
好处:字节码文件与操作系统无关(jvm屏蔽了系统差异),实现了跨平台,执行效率比传统解释型语言高,又保留了解释型语言可移植的特点
(可移植性:
由于不同平台的机器码不同,编译型语言在原本平台上编译生成的可执行文件就不能用了,必须重新编译出新的可执行文件,而解释型语言是对源代码逐行解释并执行,所以只要该平台有解释器(能将源文件编译成对应平台的机器码),同一个源文件在不同平台都可以直接解释执行 二者最大的差别是编译和执行的连续性,由于解释型语言编译完一行执行一行,没有可执行文件,所以同样的代码在不同平台也可以运行,不需要重新生成可执行文件)
1.1.4 JIT编译器(just-in-time compilation)
JIT属于运行时编译,JIT存在于JVM中
为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。
Java执行过程
一:javac将会源码编译成字节码
在这个过程中会进行词法分析、语法分析、语义分析,编译原理中这部分的编译称为前端编译
二:接下来直接将字节码解释执行
在解释执行的过程中,虚拟机同时对程序运行的信息进行收集,在这些信息的基础上,编译器会逐渐发挥作用,它会进行后端编译——把字节码编译成机器码,但不是所有的代码都会被编译,只有被JVM认定为的热点代码,才可能被编译
JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中,当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行
1.1.5 Java 和 C++ 的区别
Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:
Java 不提供指针来直接访问内存,程序内存更加安全
Java 的类是单继承的,C++ 支持多继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
c++指针和Java引用的区别
(指针也就是内存地址,指针变量是用来存放内存地址的变量)
(引用存的是对象的地址,局部变量的引用存在栈中,类变量和类的实例变量的引用存在堆中)
(Java的引用不能直接对内存地址的数据进行读写,不能指向任何一个地址(只能指向一个对象),不能修改随意所指向地址的数据(只能修改指向对象的固定成员))
类变量和实例变量的区别
类变量是类中static关键字定义的静态变量,实例变量是类的成员变量 (他俩都是全局变量)
类变量存在jvm的方法区(JDK1.7后在堆)中,实例变量存在jvm的堆中
类变量在类加载的准备阶段分配内存并设置默认值,在初始化阶段设置初始值
,而实例变量在对象初始化阶段初始化(分配内存并赋初始值)
全局变量和局部变量的区别
全局变量定义在类和接口内,局部变量定义在方法体或代码块中
1.2 基本语法
1.2.1 标识符和关键字的区别是什么
在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 标识符 。简单来说, 标识符就是一个名字 。
有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 关键字 。简单来说,关键字是被赋予特殊含义的标识符
1.2.2 静态方法为什么不能调用非静态成员
这个需要结合 JVM 的相关知识,主要原因如下:
静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
静态方法可以在对象未实例化 (也就是非静态变量未被分配内存时) 就被调用
1.2.3 重载和重写有什么区别?
重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
1.3 基本数据类型
1.3.1 Java 中的几种基本数据类型了解么?
int long boolean float double char short byte
4 种整数型:
byte
、short
、int
、long
2 种浮点型:
float
、double
1 种字符类型:
char
1 种布尔型:
boolean
。
1.3.2 基本类型和包装类型的区别?
成员变量包装类型不赋值就是
null
,而基本类型有默认值且不是null
。相比于对象类型, 基本数据类型占用的空间非常小。
在使用“==”进行判断的时候的不同
ps:List<int> 自动装箱再在泛型擦除时强转为Object
==是判断引用指向的对象在内存的地址是否相等,两个new出来的包装类对象值虽然相等,但对象不相等
.equals()可以判断两个包装类对象的内容是否相等
1.3.3 包装类型的缓存机制了解么?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
如果在范围内会使用缓存中的对象
如果超出对应范围仍然会去创建新的对象
当需要进行自动装箱时,如果数字在 -128 至 127 之间,会直接使用缓存中的对象,而不是重新创建一个对象。
1.3.4 为什么整型包装类对象之间值的比较,全部使用equals方法
不复用已有对象后,两个new出来的对象即使内容相同,==也判断地址不相等返回false
1.3.5 自动装箱与拆箱了解吗?原理是什么?
什么是自动拆装箱?
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
Integer i = 10; //装箱
int n = i; //拆箱
ps:自动装箱是通过 Integer.valueOf() 完成的;自动拆箱是通过 Integer.intValue() 完成的
泛型
没有泛型的时候,我们声明的 List 集合默认是可以存储任意类型元素的
遍历的时候我们拿到的 item 其实是 Object 类型,如果要使用就必须强转,强转就必须得判断当前元素的具体类型,否则直接使用强转很可能会发生类型转换异常。 这样不安全且不方便
我们需要一种机制能强制性的让我们只能存储对应类型的元素,否则编译就不通过,所以泛型出现了。
有了泛型的指定,我们声明的 list 就只能存储规定类型 String ,类型不匹配的问题就在编译时候就能检查出来
1.类型安全 在编译期实现类型安全检查,同时避免强转异常。这意味着,如果您使用了不正确的类型,代码将无法编译,从而避免了在运行时遇到类型错误的情况。
2.代码复用 使用泛型可以使代码更加通用和灵活。您可以编写一个通用方法,并使用不同的类型进行调用,在不同的地方重复使用该方法,以达到最大的代码重用。
总结泛型的好处
编译期类型安全,避免了强制类型转换运行时异常
同一个类可以操作多种类型数据,代码复用
定义
泛型的本质就是把类型参数化,所操作的数据类型被指定为参数,根据动态传入进行处理
泛型擦除
所谓的泛型擦除其实很简单,简单来说就是泛型只在编译时起作用,运行时泛型还是被当成 Object 来处理
Java SPI
API的英文即Application Programming Interface首字母的缩写。不要被这么长的单词吓到,直译过来的意思就是:程序之间的接口。我更倾向于把API理解为,程序之间的合约。
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
(spring对于外部jar包的引入就是SPI)
2.面向对象基础
2.1 面向对象基础
2.1.1 面向对象和面向过程的区别
区别:
面向过程的分析主体为解决问题的行为步骤,面向对象的分析主体为问题中的行动执行者和被执行者。
2.1.2 创建一个对象用什么运算符?对象实体与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(局部变量的对象引用存放在栈内存中,全局变量的对象应用存放在堆中)。
一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
2.1.2 为啥要重写equals
equals()和hashCode()都是Object类的方法
equals没重写前比较的是两个对象的引用地址是否相等,和==一样都是比较内存地址
基本数据类型的包装类一般都重写了equals方法,不比较地址比较对象内容
如果说我们想要让自定义类的对象用equals比较内容而不是地址,就要重写equals方法
为什么java中在重写equals()方法后必须对hashCode()方法进行重写? 为了维护hashCode()方法的equals协定,该协定指出:如果根据 equals()方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode方法都必须生成相同的整数结果;而两个hashCode()返回的结果相等,两个对象的equals()方法不一定相等。
所以在重写父类的equals()方法时,也重写hashcode()方法,使相等的两个对象获取的HashCode值也相等
2.1.3 对象的相等和引用相等的区别
对象的相等一般比较的是内存中存放的内容是否相等。
引用相等一般比较的是他们指向的内存地址是否相等。
2.1.4 如果一个类没有声明构造方法,该程序能正确执行吗?
如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。
2.1.5 构造方法有哪些特点?是否可被 override?
构造方法特点如下:
名字与类名相同。
没有返回值,但不能用 void 声明构造函数。
生成类的对象时自动执行,无需调用。
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
2.1.6 接口和抽象类有什么共同点和区别?
共同点 :
都不能被实例化。
都可以包含抽象方法。
都可以有默认实现的方法(Java 8 可以用
default
关键字在接口中定义默认方法)。
区别 :
抽象类的主要目的是为子类提供一个通用的模板(代码复用和约束子类),而接口是给一个方法行为提供模板
一个类只能继承一个类,但是可以实现多个接口。
接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。接口的所有方法必须在实现类被实现,抽象类的抽象方法必须被子类实现,非抽象方法在抽象类中已实现,子类可以复写
2.1.7 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
拷贝分为引用拷贝和对象拷贝。浅拷贝和深拷贝都是对象拷贝
引用拷贝:创建一个指向对象的引用变量的拷贝
Teacher teacher = new Teacher("riemann", 28);
Teacher otherTeacher = teacher;
这两个引用变量指向同一个地址,也就是指向同一个对象
对象拷贝:创建对象本身的一个副本
浅拷贝:
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。
简而言之,浅拷贝仅仅复制所拷贝的对象,而不复制它所引用的对象。
原对象引用和拷贝对象引用指向两个不同的对象,但对于对象内中其他对象的引用,二者指向的是同一个对象
深拷贝:深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
Java值传递
java传递的都是值
Java 中将实参传递给方法(或函数)的方式是 值传递:
如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本
2.2 Java常见类
2.2.1 Object
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
getClass() hashCode() equals(Object obj) clone() clone() notify() notifyAll() wait(long timeout)
wait(long timeout, int nanos) wait() finalize()
2.2.2 == 和 equals() 的区别
==
对于基本类型和引用类型的作用效果是不同的:
对于基本数据类型来说,
==
比较的是值。对于引用数据类型来说,
==
比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()
方法存在于Object
类中,而Object
类是所有类的直接或间接父类,因此所有的类都有equals()
方法。
equals()
方法存在两种使用情况:
类没有重写
equals()
方法 :通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。类重写了
equals()
方法 :一般我们都重写equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
String
中的 equals
方法是被重写过的,因为 Object
的 equals
方法是比较的对象的内存地址,而 String
的 equals
方法比较的是对象的值。
当创建 String
类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String
对象。
2.2.3 hashCode() 有什么用?
hashCode()
的作用是根据对象内存地址获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
Object
的 hashCode()
方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
# 为什么要有 hashCode?
我们以“HashSet
如何检查重复”为例子来说明为什么要有 hashCode
?
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
当你把对象加入
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较,如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。但是如果发现有相同hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
其实, hashCode()
和 equals()
都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap
、HashSet
)中,有了 hashCode()
之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet
的过程)!
我们在前面也提到了添加元素进HashSet
的过程,如果 HashSet
在对比的时候,同样的 hashCode
有多个对象,它会继续使用 equals()
来判断是否真的相同。也就是说 hashCode
帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode()
方法呢?
这是因为两个对象的hashCode
值相等并不代表两个对象就相等。
那为什么两个对象有相同的 hashCode
值,它们也不一定是相等的?
因为 hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode
)。
总结下来就是 :
如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
相信大家看了我前面对 hashCode()
和 equals()
的介绍之后,下面这个问题已经难不倒你们了。
# 为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
思考 :重写 equals()
时没有重写 hashCode()
方法的话,使用 HashMap
可能会出现什么问题。
总结 :
equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。两个对象有相同的
hashCode
值,他们也不一定是相等的(哈希碰撞)。
2.2.4 String
String、StringBuffer、StringBuilder 的区别?
可变性
String不可变,StringBuffer StringBuilder可变
他们三个都是通过字符数组保存字符串,
String不可变的原因是
保存字符串的数组被 final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。
String
类被 final
修饰导致其不能被继承,进而避免了子类破坏 String
不可变。
而StringBuilder和StringBuffer虽然也用字符数组保存字符串,不过没有用private和final修饰,而且他俩都继承自AbstractStringBuilder类,该类提供了修改字符串的方法,如append
线程安全性
String不可变,也就是常量,线程安全
StringBuilder对方法加了同步锁,所以线程安全
StringBuffer没有加锁,所以非线程安全
应用
操作少量的数据: 适用
String
单线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
多线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
String +号字符串拼接?
字符串对象通过“+”的字符串拼接方式,实际上是通过**StringBuilder**
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
编译器不会创建单个 StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。
字符串拼接建议直接使用StringBuilder
字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String s1 = new String("abc");这句话创建了几个字符串对象?
1~2个
首先new String创建一个
然后给这个String对象初始化赋初值时,如果字符串常量池中有“abc”对象,则直接使用,没有的话在堆中创建“abc”对象并保存到常量池中
2.3 序列化
序列化是否必须要实现serializable?_序列化对象 需要serializable-CSDN博客
什么是序列化? 简单来说,我们把对象从内存中变成可存储或传输的过程称之为序列化
为什么要序列化? 根本原因:需要将变量或对象从内存中取出来进行存储或传输
具体应用 对象保存到文件或数据库 RPC远程接口调用 序列化的常见形式 转换成二进制字节流的形式,主要将对象序列化成流的形式,用于数据存储 JSON序列化器,主要将对象序列化成字符串,用于数据传输 … java类需不需要实现Serializable接口? 1.转换成二进制字节流的形式
这种需要序列化的类必须实现Serializable。 常见的例子:把对象存储在Redis服务器中、RPC形式的远程方法调用(微服务使用Dubbo)
2.转换成JSON字符串的形式
这种类就不需要实现Serializable了 常见的例子:后端暴露的接口返回的JSON格式对象、HTTP形式的远程方法调用(微服务使用的Feign)
3. 异常 泛型 反射 注解 SPI 序列化
3.1 异常
在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长、臃肿,可读性差。因此采用异常处理机制。
Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅,并易于维护
3.1.1 结构
3.1.2 Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。Error
:Error
属于程序无法处理的错误 ,不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止
3.1.3 Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译。(不处理就没法过编译)
ps:分辨受检异常和语法逻辑错误 开发过程中的语法错误和逻辑错误不是异常,不能通过异常处理机制解决
在代码中,受检异常和语法、逻辑错误的报错信息是不同的。受检异常会明确提示你需要处理哪些异常,并提醒你添加try-catch块或throws语句。而语法、逻辑错误则会给出具体的错误信息,比如说“无法解析符号”或“变量未初始化”,以帮助你找到问题所在
比如下面这段 IO 操作的代码:
除了RuntimeException
及其子类以外,其他的Exception
类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException
、SQLException
...。
How:怎样处理检查异常(checked exception)?
1、继续抛出,消极的方法,一直可以抛到java虚拟机来处理,就是通过throws Exception抛出。
2、用try...catch捕获进行处理
注意,对于检查的异常必须处理,或者必须捕获或者必须抛出
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException
及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户How:对非检查的异常(unchecked exception )怎样处理?
1、用try...catch捕获 2、继续抛出 3、不处理 4、通过代码处理 一般我们是通过代码处理的,因为你很难判断会出什么问题,而且有些异常你也无法运行时处理,比如空指针,需要人手动的去查找。 而且,捕捉异常并处理的代价远远大于直接抛出。
3.1.4 Throwable 类常用方法有哪些?
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
3.1.5 try-catch-finally 如何使用?
try
块 : 用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。catch
块 : 用于处理 try 捕获到的异常。finally
块 : 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。
(finally块的代码不会执行,如果在执行前,虚拟机被终止运行,程序所在的线程死亡 )
trycatchfinally try捕获异常,catch处理异常后,执行finally代码,执行完后跳出trycatch结构,继续执行之后的代码
atch 中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面,否则报错(先捕获具体的,在捕获粗略的)
3.1.6 异常使用有哪些需要注意的地方?
不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出
NumberFormatException
而不是其父类IllegalArgumentException
。当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。
3.1.7 throws
throws + 异常类型
throws + 异常类型,写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。一旦当方法执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足 throws 后异常类型时,就会被抛出。异常代码后续的代码,就不再执行
throws 方式只是将异常抛给了方法的调用者Caller去解决,并没有真正将异常处理
如何选择使用哪种方式(trycatch还是throws)处理异常
如果父类中被重写的方法没有 throws 方式处理异常,则子类重写的方法也不能使用 throws ,意味着如果子类重写的方法中有异常,必须使用 try-catch-finally 方式处理
执行的方法 a 中,先后又调用了另外的几个方法,这几个方法是递进关系执行。我们建议这几个方法使用 throws 的方法进行处理。而执行的方法 a 可以考虑使用 try-catch-finally 方式进行处理
3.1.8 throw手动抛出异常
Java异常类对象除在程序执行过程中出现异常时由系统自动生成并抛出,也可根据需要使用人工创建并抛出。
首先要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。
throw exceptionObject
程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面可以抛出的异常必须是Throwable或其子类的实例
throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。
手动抛出的异常必须使用:trycatch 或者全局异常处理类捕获处理
3.1.9 异常处理的处理机制
在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长、臃肿,可读性差。因此采用异常处理机制。
在编写代码处理异常时,对于检查异常/非检查异常,都有2种不同的处理方式: 1、使用try...catch...finally语句块处理它。 2、在函数签名中使用throws 声明交给函数调用者caller去解决。
比如现有一辆车,这个车你可以是方法,这辆车在可能存在各种风险,那么对于这些风险的处理方式,就相当于异常的处理方式: 1、使用try...catch...finally语句块处理它。 我们把这辆车可能出现的问题都考虑清楚了,并提供了备选方案(出现问题怎么做),如果没有出现问题,那么用不到备选方案; 如果出现了问题,根据问题去找对应的备选方案,以保证车的正常运行; 如果出现了问题,但是又没备选方案,那么车就跑不了; 2、在函数签名中使用throws 声明交给函数调用者caller去解决。 我知道车可能又问题,但是我不处理,谁来使用了,告诉调用者,这里可能有问题; 那么调用者可以处理这个问题,也可以不处理;如果它不处理,还是会出现问题,如果处理了,肯定没问题; A -》 B -》 C
Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅,并易于维护。
Java提供的是异常处理的抓抛模型。
Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常。
异常对象的生成: 由虚拟机自动生成(自动抛出):程序运行过程中,虚拟机检测到程序发生了问题,就生成一个异常对象,要是有trycatch,就在catch块匹配,若异常对象与catch块中的异常参数匹配,就在catch块进行处理,否则会继续向上层调用栈传递,直到找到能够捕获该异常的try-catch块或者程序终止。 由开发人员手动创建:Exception exception = new ClassCastException();——创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样。
异常的抛出机制: 如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。 如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。(多线程只会关闭当前线程,不会关闭程序) 程序员通常只能处理Exception,而对Error无能为力。
ps:非受检异常即使没有trycatch捕获,jvm也可以进行处理,但是会使程序终止,受检异常不许被捕获,否则无法过编译
3.1.10 异常链化
一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。
异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫它根源异常(cause)。
4.Java8 新特性
Lambda
使用 Lambda 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程。
语法格式
(parameters) -> expression 或
(parameters) ->{ statements; }
应用
用来替代匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("The runable now is using!");
}
}).start();
//用lambda
new Thread(() -> System.out.println("It's a lambda function!")).start();
List<Integer> strings = Arrays.asList(1, 2, 3);
Collections.sort(strings, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;}
});
//Lambda
Collections.sort(strings, (Integer o1, Integer o2) -> o1 - o2);
//分解开
Comparator<Integer> comparator = (Integer o1, Integer o2) -> o1 - o2;
Collections.sort(strings, comparator);
用来集合迭代
void lamndaFor() {
List<String> strings = Arrays.asList("1", "2", "3");
//传统foreach
for (String s : strings) {
System.out.println(s);
}
//Lambda foreach
strings.forEach((s) -> System.out.println(s));
//or
strings.forEach(System.out::println);
//map
Map<Integer, String> map = new HashMap<>();
map.forEach((k,v)->System.out.println(v));
}
Stream
java 新增了 java.util.stream
包,
Stream
不存储数据,不同的是它可以检索(Retrieve)和逻辑处理集合数据、包括筛选、排序、统计、计数等。可以想象成是 Sql 语句。
它的源数据可以是 Collection
、Array
等。由于它的方法参数都是函数式接口类型,所以一般和 Lambda 配合使用
流类型
stream 串行流
parallelStream 并行流,可多线程执行
常用方法
/**
* 返回一个串行流
*/
default Stream<E> stream()
/**
* 返回一个并行流
*/
default Stream<E> parallelStream()
/**
* 返回T的流
*/
public static<T> Stream<T> of(T t)
/**
* 返回其元素是指定值的顺序流。
*/
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
/**
* 过滤,返回由与给定predicate匹配的该流的元素组成的流
*/
Stream<T> filter(Predicate<? super T> predicate);
/**
* 此流的所有元素是否与提供的predicate匹配。
*/
boolean allMatch(Predicate<? super T> predicate)
/**
* 此流任意元素是否有与提供的predicate匹配。
*/
boolean anyMatch(Predicate<? super T> predicate);
/**
* 返回一个 Stream的构建器。
*/
public static<T> Builder<T> builder();
/**
* 使用 Collector对此流的元素进行归纳
*/
<R, A> R collect(Collector<? super T, A, R> collector);
/**
* 返回此流中的元素数。
*/
long count();
/**
* 返回由该流的不同元素(根据 Object.equals(Object) )组成的流。
*/
Stream<T> distinct();
/**
* 遍历
*/
void forEach(Consumer<? super T> action);
/**
* 用于获取指定数量的流,截短长度不能超过 maxSize 。
*/
Stream<T> limit(long maxSize);
/**
* 用于映射每个元素到对应的结果
*/
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
/**
* 根据提供的 Comparator进行排序。
*/
Stream<T> sorted(Comparator<? super T> comparator);
/**
* 在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。
*/
Stream<T> skip(long n);
/**
* 返回一个包含此流的元素的数组。
*/
Object[] toArray();
/**
* 使用提供的 generator函数返回一个包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。
*/
<A> A[] toArray(IntFunction<A[]> generator);
/**
* 合并流
*/
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
实战
@Test
public void test() {
List<String> strings = Arrays.asList("abc", "def", "gkh", "abc");
//返回符合条件的stream
Stream<String> stringStream = strings.stream().filter(s -> "abc".equals(s));
//计算流符合条件的流的数量
long count = stringStream.count();
//forEach遍历->打印元素
strings.stream().forEach(System.out::println);
//limit 获取到1个元素的stream
Stream<String> limit = strings.stream().limit(1);
//toArray 比如我们想看这个limitStream里面是什么,比如转换成String[],比如循环
String[] array = limit.toArray(String[]::new);
//map 对每个元素进行操作返回新流
Stream<String> map = strings.stream().map(s -> s + "22");
//sorted 排序并打印
strings.stream().sorted().forEach(System.out::println);
//Collectors collect 把abc放入容器中
List<String> collect = strings.stream().filter(string -> "abc".equals(string)).collect(Collectors.toList());
//把list转为string,各元素用,号隔开
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(","));
//对数组的统计,比如用
List<Integer> number = Arrays.asList(1, 2, 5, 4);
IntSummaryStatistics statistics = number.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : "+statistics.getMax());
System.out.println("列表中最小的数 : "+statistics.getMin());
System.out.println("平均数 : "+statistics.getAverage());
System.out.println("所有数之和 : "+statistics.getSum());
//concat 合并流
List<String> strings2 = Arrays.asList("xyz", "jqx");
Stream.concat(strings2.stream(),strings.stream()).count();
//注意 一个Stream只能操作一次,不能断开,否则会报错。
Stream stream = strings.stream();
//第一次使用
stream.limit(2);
//第二次使用
stream.forEach(System.out::println);
//报错 java.lang.IllegalStateException: stream has already been operated upon or closed
//但是可以这样, 连续使用
stream.limit(2).forEach(System.out::println);
}
steam.anyMatch()
anyMatch:判断的条件里,任意一个元素成功,返回true
allMatch:判断条件里的元素,所有的都是,返回true
noneMatch:与allMatch相反,判断条件里的元素,所有的都不是,返回true
//将学生姓名放到Set中,可以实现去重功能
Set<String> studentNames=students.stream().map(student -> student.getName()).collect(Collectors.toSet())
.map
map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5); // 获取对应的平方数 List<Integer> squaresList = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
Optional
建议使用 Optional
解决 NPE(java.lang.NullPointerException
)问题,它就是为 NPE 而生的,其中可以包含空值或非空值
5.设计模式
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
设计原则
开闭原则
模块应对扩展开放,对修改关闭,也就是在对程序进行拓展的时候,不要去修改原来的代码,实现热插拔的效果。这样可以使得程序扩展性更好,更易于维护。(主要是使用接口和抽象类实现,例如“抽象工厂模式”)
里氏替换原则
任何父类出现的地方,子类一定可以出现,就是用子类替换也一定可以运行。(子类可以扩展父类但不能改变父类的功能)(里氏替换原则可以说继承复用的基础
依赖倒转原则
程序要依赖于抽象接口,不要依赖于具体实现。(开闭原则的基础)针对接口编程,依赖抽象类而不依赖具体类。
接口隔离原则
使用多个隔离的接口,比使用单个接口要好。其实就降低程序之间的耦合度,增加系统的可维护性。
最少知道原则(迪米特法则)
一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。也是降低类之间的耦合度。
合成复用原则
尽量使用合成/聚合的方式,而不是使用继承。某些情景下可以在一个新对象里面使用一些已有的对象达到复用的作用,而不是通过继承的方式,这样如果已有的类要进行改动就不需要对所有的类进行改动了。
这里只讲工厂,单例,代理模式
1 工厂模式
1.1 简单工厂模式 定义:定义了一个创建对象的类,由这个类来封装实例化对象的行为。
举例:(我们举一个pizza工厂的例子)
pizza工厂一共生产三种类型的pizza:chesse,pepper,greak。通过工厂类(SimplePizzaFactory)实例化这三种类型的对象。类图如下:
工厂类的代码:
public class SimplePizzaFactory { public Pizza CreatePizza(String ordertype) { Pizza pizza = null; if (ordertype.equals("cheese")) { pizza = new CheesePizza(); } else if (ordertype.equals("greek")) { pizza = new GreekPizza(); } else if (ordertype.equals("pepper")) { pizza = new PepperPizza(); } return pizza; } } 简单工厂存在的问题与解决方法: 简单工厂模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了开闭原则,所以,从设计角度考虑,有一定的问题,如何解决?我们可以定义一个创建对象的抽象方法并创建多个不同的工厂类实现该抽象方法,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。这种方法也就是我们接下来要说的工厂方法模式。
1.2 工厂方法模式 定义:定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。
举例:(我们依然举pizza工厂的例子,不过这个例子中,pizza产地有两个:伦敦和纽约)。添加了一个新的产地,如果用简单工厂模式的的话,我们要去修改工厂代码,并且会增加一堆的if else语句。而工厂方法模式克服了简单工厂要修改代码的缺点,它会直接创建两个工厂,纽约工厂和伦敦工厂。类图如下:
OrderPizza中有个抽象的方法:
abstract Pizza createPizza(); 两个工厂类继承OrderPizza并实现抽象方法:
public class LDOrderPizza extends OrderPizza { Pizza createPizza(String ordertype) { Pizza pizza = null; if (ordertype.equals("cheese")) { pizza = new LDCheesePizza(); } else if (ordertype.equals("pepper")) { pizza = new LDPepperPizza(); } return pizza; } } public class NYOrderPizza extends OrderPizza {
Pizza createPizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new NYCheesePizza();
} else if (ordertype.equals("pepper")) {
pizza = new NYPepperPizza();
}
return pizza;
}
} 、通过不同的工厂会得到不同的实例化的对象,PizzaStroe的代码如下:
public class PizzaStroe { public static void main(String[] args) { OrderPizza mOrderPizza; mOrderPizza = new NYOrderPizza(); } } 解决了简单工厂模式的问题:增加一个新的pizza产地(北京),只要增加一个BJOrderPizza类:
public class BJOrderPizza extends OrderPizza { Pizza createPizza(String ordertype) { Pizza pizza = null; if (ordertype.equals("cheese")) { pizza = new LDCheesePizza(); } else if (ordertype.equals("pepper")) { pizza = new LDPepperPizza(); } return pizza; } } 其实这个模式的好处就是,如果你现在想增加一个功能,只需做一个实现类就OK了,无需去改动现成的代码。这样做,拓展性较好!
工厂方法存在的问题与解决方法:客户端需要创建类的具体的实例。简单来说就是用户要订纽约工厂的披萨,他必须去纽约工厂,想订伦敦工厂的披萨,必须去伦敦工厂。 当伦敦工厂和纽约工厂发生变化了,用户也要跟着变化,这无疑就增加了用户的操作复杂性。为了解决这一问题,我们可以把工厂类抽象为接口,用户只需要去找默认的工厂提出自己的需求(传入参数),便能得到自己想要产品,而不用根据产品去寻找不同的工厂,方便用户操作。这也就是我们接下来要说的抽象工厂模式。
1.3 抽象工厂模式 定义:定义了一个接口用于创建相关或有依赖关系的对象族,而无需明确指定具体类。
举例:(我们依然举pizza工厂的例子,pizza工厂有两个:纽约工厂和伦敦工厂)。类图如下:
工厂的接口:
public interface AbsFactory { Pizza CreatePizza(String ordertype) ; } 工厂的实现:
public class LDFactory implements AbsFactory { @Override public Pizza CreatePizza(String ordertype) { Pizza pizza = null; if ("cheese".equals(ordertype)) { pizza = new LDCheesePizza(); } else if ("pepper".equals(ordertype)) { pizza = new LDPepperPizza(); } return pizza; } } PizzaStroe的代码如下:
public class PizzaStroe { public static void main(String[] args) { OrderPizza mOrderPizza; mOrderPizza = new OrderPizza("London"); } } 解决了工厂方法模式的问题:在抽象工厂中PizzaStroe中只需要传入参数就可以实例化对象。
1.4 工厂模式适用的场合 大量的产品需要创建,并且这些产品具有共同的接口 。
1.5 三种工厂模式的使用选择 简单工厂 : 用来生产同一等级结构中的任意产品。(不支持拓展增加产品)
工厂方法 :用来生产同一等级结构中的固定产品。(支持拓展增加产品)
抽象工厂 :用来生产不同产品族的全部产品。(支持拓展增加产品;支持增加产品族)
简单工厂的适用场合:只有伦敦工厂(只有这一个等级),并且这个工厂只生产三种类型的pizza:chesse,pepper,greak(固定产品)。
工厂方法的适用场合:现在不光有伦敦工厂,还增设了纽约工厂(仍然是同一等级结构,但是支持了产品的拓展),这两个工厂依然只生产三种类型的pizza:chesse,pepper,greak(固定产品)。
抽象工厂的适用场合:不光增设了纽约工厂(仍然是同一等级结构,但是支持了产品的拓展),这两个工厂还增加了一种新的类型的pizza:chinese pizza(增加产品族)。
所以说抽象工厂就像工厂,而工厂方法则像是工厂的一种产品生产线。因此,我们可以用抽象工厂模式创建工厂,而用工厂方法模式创建生产线。比如,我们可以使用抽象工厂模式创建伦敦工厂和纽约工厂,使用工厂方法实现cheese pizza和greak pizza的生产。类图如下:
总结一下三种模式:
简单工厂模式就是建立一个实例化对象的类,在该类中对多个对象实例化。工厂方法模式是定义了一个创建对象的抽象方法,由子类决定要实例化的类。这样做的好处是再有新的类型的对象需要实例化只要增加子类即可。抽象工厂模式定义了一个接口用于创建对象族,而无需明确指定具体类。抽象工厂也是把对象的实例化交给了子类,即支持拓展。同时提供给客户端接口,避免了用户直接操作子类工厂。
应用
在Spring中,工厂模式用于根据应用程序当前状态动态创建bean实例
BeanFactory是访问Spring容器的核心接口,负责创建和管理bean对象。BeanFactory使用多种不同策略来创建和管理bean,包括Singleton和Prototype设计模式。
使用:
我们可以把一个类注册为一个bean放到IOC容器中
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserServiceImpl();
}
}
当我们想要在应用程序中使用UserService对象时,我们可以从Spring容器中使用BeanFactory获取它
public class UserController {
private BeanFactory beanFactory;
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
public void doSomething() {
UserService userService = (UserService) beanFactory.getBean("userService");
// use userService object here
}
}
2 单例模式
定义:
确保一个类最多只有一个实例,由自行创建,并提供一个全局访问点
核心精髓其实是避免创建不必要的对象
应用
常见的使用场合:数据库的连接池、Spring中的全局访问点BeanFactory,Spring下的Bean(IOC容器中的每个Bean都是全局唯一的)、多线程的线程池、网络连接池
单例与spring
在Spring中,默认创建单例对象。这意味着每个Spring上下文只创建一个特定bean的实例
Spring将只创建一个bean实例并将其缓存。对bean的任何后续请求都将返回缓存的实例。
可以在类上使用@Component注解或其原型注解(@Service、@Repository等)实现单例Bean。
@Component
public class MySingletonBean {
// implementation here
}
优缺点
减少每次创建对象的时间开销,还可以节约内存空间;
避免由于操作多个实例导致的逻辑错误
如果一个对象有可能贯穿整个应用程序,能够起到了全局统一管理控制的作用。
分类与实现
单例模式可以分为两种:预加载和懒加载
2.1 预加载 顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。
public class PreloadSingleton {
public static PreloadSingleton instance = new PreloadSingleton();
//其他的类无法实例化单例类的对象
private PreloadSingleton() {
};
public static PreloadSingleton getInstance() {
return instance;
}
}
很明显,没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。
2.2 懒加载 为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建。
public class SingletonEasy {
private static SingletonEasy instance;
private SingletonEasy() {}//将构造器 私有化,防止外部调用
public static SingletonEasy getInstance() {
if (instance == null) {
instance = new SingletonEasy();
}
return instance;
}
}
懒加载和线程安全
懒加载不浪费内存,但是无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的,有可能两个线程同时发现不存在实例,都去尝试创建实例了。
其次,new Singleton()无法保证执行的顺序性。
为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:
memory=allocate();//1:初始化内存空间 ctorInstance(memory);//2:初始化对象 instance=memory();//3:设置instance指向刚分配的内存地址
jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面2和3行代码可能被重新排序。
懒汉+线程安全
synchronized
public class SingleSyn {
private static SingleSyn instance;
private SingleSyn() {//将构造器 私有化,防止外部调用
}
public static synchronized SingleSyn getInstance(){
if (instance == null) {
instance = new SingleSyn();
}
return instance;
}
性能低
双重校验锁 synchronized+volatile版
ps:双重校验同步锁为了 让不需要初始化实例的请求能直接不上锁 同时拿实例, 只有需要instance为空才上锁
volatile为了防止指令重排序,导致 创建对象时
instance = new SingleDoubleCheck()
初始化对象和设置intance引用指向对应内存地址乱序执行,
存在一个instance已经不为null但是SingleDoubleCheck仍没有完成初始化
的状态这个时候其他的线程过来,走到if (instance == null)
处时会产生:明明instance不为空,但是SingleDoubleCheck却没有初始化的问题
public class SingleVolatile {
private static volatile SingleVolatile instance;// 加上volatile关键字
private SingleVolatile() {}//将构造器 私有化,防止外部调用
public static SingleVolatile getInstance() {
if (instance == null) {
synchronized (SingleVolatile.class) {
if (instance == null) {
instance = new SingleVolatile();
}
}
}
return instance;
}
}
3 代理模式
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用
中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用
开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则
代理模式分为三类:1. 静态代理 2. 动态代理 3. CGLIB代理
静态代理
缺点: 代理对象与目标对象要实现相同的接口,我们得为每一个服务都得创建代理类,工作量太大
我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(*比如接口一旦新增加方法,目标对象和代理对象都要进行修改*)且麻烦(*需要对每个目标类都单独写一个代理类*)
动态代理
我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。
动态代理是在运行时动态生成类字节码,并加载到 JVM 中的
应用:Spring AOP、RPC 框架实现都依赖了动态代理。
Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理
JDK动态代理
要求被代理对象必须有接口。
第一步:编写动态处理器
*(这个处理器是可以复用的,很多类的动态代理都能用)
public class DynamicProxyHandler implements InvocationHandler {
private Object object;
public DynamicProxyHandler(final Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("买房前准备");
Object result = method.invoke(object, args);
System.out.println("买房后装修");
return result;
}
}
invoke方法还拓展了 功能
第二步:编写测试类
public class DynamicProxyTest {
public static void main(String[] args) {
BuyHouse buyHouse = new BuyHouseImpl();
BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(BuyHouse.class.getClassLoader(), new
Class[]{BuyHouse.class}, new DynamicProxyHandler(buyHouse));
proxyBuyHouse.buyHosue();
}
}
CGLIB代理
不需要被代理的对象有接口
1.自定义 MethodInterceptor
/**
* 自定义MethodInterceptor
*/
public class DebugMethodInterceptor implements MethodInterceptor {
/**
* @param o 被代理的对象(需要增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return object;
}
}
2,获取代理类并实际使用
import net.sf.cglib.proxy.Enhancer;
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
实际使用
AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("java");
其实一样的,都是实现拦截器,然后 代理工厂类调用getProxy方法获得代理类对象,然后用代理类对象调用方法完成操作
其实就是自定义实现拦截器和代理工厂类,这样很多类我们都可以直接调用代理工厂类的方法创建代理类对象
对比
JDK 动态代理和 CGLIB 动态代理对比
JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。
就二者的效率来说,大部分情况都是 JDK 动态代理更优秀
静态代理和动态代理的对比
灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。
另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的
6.反射
赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射你可以在运行时获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
原理
反射与 java.lang.Class
描述一个类的信息的类, 由类加载器在类加载过程中放到堆里
Class对象是所有反射API的入口点。
使用反射时需要先从Class对象中取出对应的filed/method 对象信息
应用场景
1.框架:Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
2.动态代理:
3.注解:
注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执
行行为
反射优点:更灵活的编码
反射缺点:有可能不安全,比如反射跳过泛型安全检查
实战
获取对象的四种方式
1. 知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class;
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
2. 通过 Class.forName()
传入类的全路径获取:
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
3. 通过对象实例instance.getClass()
获取:
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
4. 通过类加载器xxxClassLoader.loadClass()
传入类路径获取:
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行
反射基本操作
创建一个我们要使用反射操作的目标类
TargetObject
。
package cn.javaguide;
public class TargetObject {
private String value;
public TargetObject() {
value = "JavaGuide";
}
public void publicMethod(String s) {
System.out.println("I love " + s);
}
private void privateMethod() {
System.out.println("value is " + value);
}
}
2.使用反射操作这个类的方法以及参数
package cn.javaguide;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {
/**
* 获取 TargetObject 类的 Class 对象并且创建 TargetObject 类实例
*/
Class<?> targetClass = Class.forName("cn.javaguide.TargetObject");
TargetObject targetObject = (TargetObject) targetClass.newInstance();
/**
* 获取 TargetObject 类中定义的所有方法
*/
Method[] methods = targetClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
/**
* 获取指定方法并调用
*/
Method publicMethod = targetClass.getDeclaredMethod("publicMethod",
String.class);
publicMethod.invoke(targetObject, "JavaGuide");
/**
* 获取指定参数并对参数进行修改
*/
Field field = targetClass.getDeclaredField("value");
//为了对类中的参数进行修改我们取消安全检查
field.setAccessible(true);
field.set(targetObject, "JavaGuide");
/**
* 调用 private 方法
*/
Method privateMethod = targetClass.getDeclaredMethod("privateMethod");
//为了调用private方法我们取消安全检查
privateMethod.setAccessible(true);
privateMethod.invoke(targetObject);
}
}
电梯导出实现
思路:
通过反射获取传入Collection<T> dataset对象的 字段上的自定义注解 从而获得 导出单元格的名称
自定义注解:
/**
* 标题注解类
* @author tangyb
* @date 2020/03/27
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelAnnotation {
// excel导出时标题显示的名字,如果没有设置Annotation属性,将不会被导出和导入
public String exportName();
}
流程:
1,查库,调用方法传参 集合Collection<T> dataset
2,遍历器next获得第一个泛型对象实体类
Iterator<T> its = dataset.iterator();
T ts = (T) its.next();
3,写表格标题行
借助反射机制拿到定义在实体类上的所有字段field,field.getAnnotation根据field上的注解@ExcelAnnotation(exportName = "保险单位") 拿到表格标题行(也就是第一行)的每个标题内容,以及反射拿到该字段对应的get方法,
4.写表格内容
然后循环遍历,从第二行开始写单元格
public void exportExcel(String title, Collection<T> dataset,
OutputStream out) {
// 声明一个工作薄
try {
//首先检查数据看是否是正确的
Iterator<T> its = dataset.iterator();
if(dataset.isEmpty()||!its.hasNext()||title==null||out==null)
{
throw new Exception("传入的数据不对!");
}
//取得实际泛型类
T ts = (T) its.next();
Class<? extends Object> tCls = ts.getClass();
HSSFWorkbook workbook = new HSSFWorkbook();
// 生成一个表格
HSSFSheet sheet = workbook.createSheet(title);
// 设置表格默认列宽度为15个字节
sheet.setDefaultColumnWidth(15);
// 生成一个样式
HSSFCellStyle style = workbook.createCellStyle();
// 设置标题样式
style = ExcelStyle.setHeadStyle(workbook, style);
// 生成一个样式
HSSFCellStyle style2 = workbook.createCellStyle();
// 设置主体样式
style2 = ExcelStyle.setbodyStyle(workbook, style2);
// 得到所有字段
Field filed[] = ts.getClass().getDeclaredFields();
// 标题
List<String> exportfieldtile = new ArrayList<String>();
// 导出的字段的get方法
List<Method> methodObj = new ArrayList<Method>();
// 遍历整个filed
for (int i = 0; i < filed.length; i++) {
Field f = filed[i];
ExcelAnnotation exa = f.getAnnotation(ExcelAnnotation.class);
// 如果设置了annottion
if (exa != null) {
String exprot = exa.exportName();
// 添加到标题
exportfieldtile.add(exprot);
// 添加到需要导出的字段的方法
String fieldname = f.getName();
String getMethodName = "get"
+ fieldname.substring(0, 1).toUpperCase()
+ fieldname.substring(1);
Method getMethod = tCls.getMethod(getMethodName,
new Class[] {});
methodObj.add(getMethod);
}
}
// 产生表格标题行
HSSFRow row = sheet.createRow(0);
for (int i = 0; i < exportfieldtile.size(); i++) {
HSSFCell cell = row.createCell(i);
cell.setCellStyle(style);
HSSFRichTextString text = new HSSFRichTextString(
exportfieldtile.get(i));
cell.setCellValue(text);
}
int index = 0;
// 循环整个集合
its = dataset.iterator();
while (its.hasNext()) {
//从第二行开始写,第一行是标题
index++;
row = sheet.createRow(index);
T t = (T) its.next();
for (int k = 0; k < methodObj.size(); k++) {
HSSFCell cell = row.createCell(k);
cell.setCellStyle(style2);
Method getMethod=methodObj.get(k);
Object value = getMethod.invoke(t, new Object[] {});
String textValue = getValue(value);
cell.setCellValue(textValue);
cell.setCellType(CellType.STRING);
}
}
workbook.write(out);
} catch (Exception e) {
e.printStackTrace();
}
}