Java基础知识
Java概述
1.如何理解Java
Java是由Sun公司于1995年5月推出的面向对象的程序设计语言。
Java继承了C++语言面向对象技术的核心,又舍弃了C++语言中的指针、运算符重载以及多重继承的特性,同时引入了泛型编程、类型安全的枚举等特性,使Java成为简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和动态的语言。
随着互联网的迅猛发展,Java已经成为重要的网络编程语言,被广泛应用于企业级Web应用开发和移动应用开发。
2.Java三大版本
Java SE
Java SE是java的标准版,主要应用于桌面应用程序的开发,同时也是java的基础,包含了java的语言基础、JDBC(java数据库连接)操作、I/O(输入/输出)、网络通信、多线程等编程技术。
Java EE
Java EE是java的企业版,主要应用于开发企业级分布式网络程序,如电子商务网站和ERP(企业资源规划)系统等,核心为EJB(企业java组件模型)
Java ME
Java ME主要应用于嵌入式系统开发,比如掌上电脑、手机等移动通信电子设备,现在基本上大部分手机厂商生产的手机都支持java技术,所以这个版本也就没人用了
3.JVM、JRE和JDK的关系
JVM
Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。
JRE
Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包
如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JDK
Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)
4.Java语言特点
简单性:Java没有像C++那样的指针,运算符重载,类的多继承。并且实现了垃圾的自动回收,简化了程序开发者对于内存管理的工作。
面向对象:封装、继承、多态
平台无关性:Java虚拟机实现平台无关性
支持网络编程并且很方便:Java语言诞生本身就是为简化网络编程设计的
支持多线程:多线程机制使应用程序在同一时间并行执行多项任
健壮性:Java语言的强类型机制、异常处理、垃圾的自动收集等
面向对象
1.面向对象和面向过程的区别
面向过程:面向过程是一种以过程为中心的编程思想,它首先分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,在使用时依次调用,是一种基础的顺序的思维方式。面向过程开发方式是对计算机底层结构的一层抽象,它将程序分为数据和操纵数据的操作两部分,其核心问题是数据结构和算法的开发和优化。常见的支持面向过程的编程语言有 C语言、COBOL 语言等。
面向对象:面向对象是按人们认识客观世界的系统思维方式,采用基于对象(实体)的概念建立模型,模拟客观世界分析、设计、实现软件的编程思想,通过面向对象的理念使计算机软件系统能与现实世界中的系统一一对应。
2.三大特性
抽象:就是忽略一个主题中与当前目标无关的方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。
封装:封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
- 子类拥有父类非 private 的属性和方法。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。(以后介绍)。
多态:所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
3.多态的实现
多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。
静态绑定与动态绑定
JVM 的方法调用指令有五个,分别是:
invokestatic:调用静态方法;
invokespecial:调用实例构造器方法、私有方法和父类方法;
invokevirtual:调用虚方法;
invokeinterface:调用接口方法,运行时确定具体实现;
invokedynamic:运行时动态解析所引用的方法,然后再执行,用于支持动态类型语言。
其中,invokestatic 和 invokespecial 用于静态绑定,invokevirtual 和 invokeinterface 用于动态绑定。可以看出,动态绑定主要应用于虚方法和接口方法。
静态绑定在编译期就已经确定,这是因为静态方法、构造器方法、私有方法和父类方法可以唯一确定。这些方法的符号引用在类加载的解析阶段就会解析成直接引用。因此这些方法也被称为非虚方法,与之相对的便是虚方法。
虚方法的方法调用与方法实现的关联(也就是分派)有两种,一种是在编译期确定,被称为静态分派,比如方法的重载;一种是在运行时确定,被称为动态分派,比如方法的覆盖。对象方法基本上都是虚方法。
这里需要特别说明的是,final 方法由于不能被覆盖,可以唯一确定,因此 Java 语言规范规定 final 方法属于非虚方法,但仍然使用 invokevirtual 指令调用。静态绑定、动态绑定的概念和虚方法、非虚方法的概念是两个不同的概念。
多态的实现
虚拟机栈中会存放当前方法调用的栈帧,在栈帧中,存储着局部变量表、操作栈、动态连接 、返回地址和其他附加信息。多态的实现过程,就是方法调用动态分派的过程,通过栈帧的信息去找到被调用方法的具体实现,然后使用这个具体实现的直接引用完成方法调用。
以 invokevirtual 指令为例,在执行时,大致可以分为以下几步:
- 先从操作栈中找到对象的实际类型 class;
- 找到 class 中与被调用方法签名相同的方法,如果有访问权限就返回这个方法的直接引用,如果没有访问权限就报错 java.lang.IllegalAccessError ;
- 如果第 2 步找不到相符的方法,就去搜索 class 的父类,按照继承关系自下而上依次执行第 2 步的操作;
- 如果第 3 步找不到相符的方法,就报错 java.lang.AbstractMethodError ;
可以看到,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。
实际上,商用虚拟机为了保证性能,通常会使用虚方法表和接口方法表,而不是每次都执行一遍上面的步骤。以虚方法表为例,虚方法表在类加载的解析阶段填充完成,其中存储了所有方法的直接引用。也就是说,动态分派在填充虚方法表的时候就已经完成了。
在子类的虚方法表中,如果子类覆盖了父类的某个方法,则这个方法的直接引用指向子类的实现;而子类没有覆盖的那些方法,比如 Object 的方法,直接引用指向父类或 Object 的实现。
4.面向对象的五大基本原则
- 单一职责原则SRP(Single Responsibility Principle)
类的功能要单一,不能包罗万象,跟杂货铺似的。 - 开放封闭原则OCP(Open-Close Principle)
一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。 - 里式替换原则LSP(the Liskov Substitution Principle LSP)
子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。 - 依赖倒置原则DIP(the Dependency Inversion Principle DIP)
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。 - 接口分离原则ISP(the Interface Segregation Principle ISP)
设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。
基础语法及常见面试题
1.数据类型
基本数据类型
引用数据类型
String 类 接口 数组
2.访问修饰符
private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
public : 对所有类可见。使用对象:类、接口、变量、方法
3.final finally finalize区别
- final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
- finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
- finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
4.static
static的意义
static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
static的特点
1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
5.equals和hashCode
- 如果两个对象相等,则 hashcode ⼀定也是相同的
- 两个对象相等,对两个对象分别调⽤ equals ⽅法都返回 true
- 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
- 因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖
- hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode(),则该 class
的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)
6.==和equals
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型比较的是值,引用数据类型比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。
7.String 和 StringBuffer、StringBuilder 的区别是什么
可变性
String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]
,所以 String 对象是不可变的。
private final char value[];
而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
但是没有用 final 关键字修饰,所以这两种对象都是可变的。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。**StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。**StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用 String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
8.反射
1 反射机制介绍
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
2 静态编译和动态编译
- **静态编译:**在编译时确定类型,绑定对象
- **动态编译:**运行时确定类型,绑定对象
3 反射机制优缺点
- 优点: 运行期类型的判断,动态加载类,提高代码灵活度。
- 缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
4 反射的应用场景
反射是框架设计的灵魂。
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
举例:① 我们在使用 JDBC 连接数据库时使用 Class.forName()
通过反射加载数据库的驱动程序;②Spring 框架也用到很多反射机制,最经典的就是 xml 的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中;2)Java 类里面解析 xml 或 properties 里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的 Class 实例; 4)动态配置实例的属性
* 获取Class对象的方式:
1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象
* 多用于配置文件,将类名定义在配置文件中。读取文件,加载类
2. 类名.class:通过类名的属性class获取
* 多用于参数的传递
3. 对象.getClass():getClass()方法在Object类中定义着。
* 多用于对象的获取字节码的方式
* 结论:
同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
* Class对象功能:
* 获取功能:
1. 获取成员变量们
* Field[] getFields() :获取所有public修饰的成员变量
* Field getField(String name) 获取指定名称的 public修饰的成员变量
* Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
* Field getDeclaredField(String name)
2. 获取构造方法们
* Constructor<?>[] getConstructors()
* Constructor<T> getConstructor(类<?>... parameterTypes)
* Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
* Constructor<?>[] getDeclaredConstructors()
3. 获取成员方法们:
* Method[] getMethods()
* Method getMethod(String name, 类<?>... parameterTypes)
* Method[] getDeclaredMethods()
* Method getDeclaredMethod(String name, 类<?>... parameterTypes)
4. 获取全类名
* String getName()
public class ConstructorTest {
/*
* 通过Class对象可以获取某个类中的:构造方法、成员变量、成员方法;并访问成员;
*
* 1.获取构造方法:
* 1).批量的方法:
* public Constructor[] getConstructors():所有"公有的"构造方法
public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有)
* 2).获取单个的方法,并调用:
* public Constructor getConstructor(Class... parameterTypes):获取单个的"公有的"构造方法:
* public Constructor getDeclaredConstructor(Class... parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有;
*
* 2.创建对象
* Constructor对象调用newInstance(Object... initargs)
*/
public static void main(String[] args) throws Exception {
//1.加载Class对象
Class clazz = Class.forName("pojo.Hero");
//2.获取所有公有构造方法
System.out.println("**********************所有公有构造方法*********************************");
Constructor[] conArray = clazz.getConstructors();
for(Constructor c : conArray){
System.out.println(c);
}
System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************");
conArray = clazz.getDeclaredConstructors();
for(Constructor c : conArray){
System.out.println(c);
}
System.out.println("*****************获取公有、无参的构造方法*******************************");
Constructor con = clazz.getConstructor(null);
//1>、因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
//2>、返回的是描述这个无参构造函数的类对象。
System.out.println("con = " + con);
//调用构造方法
Object obj = con.newInstance();
System.out.println("******************获取私有构造方法,并调用*******************************");
con = clazz.getDeclaredConstructor(float.class);
System.out.println(con);
//调用构造方法
con.setAccessible(true);//暴力访问(忽略掉访问修饰符)
obj = con.newInstance(100);
}
}
* Field:成员变量
* 操作:
1. 设置值
* void set(Object obj, Object value)
2. 获取值
* get(Object obj)
3. 忽略访问权限修饰符的安全检查
* setAccessible(true):暴力反射
public class ParaTest {
public static void main(String[] args) {
HeroPlus h =new HeroPlus();
//使用传统方式修改name的值为garen
h.name = "garen";
try {
//获取类HeroPlus的名字叫做name的字段
Field f1= h.getClass().getDeclaredField("name");
//修改这个字段的值
f1.set(h, "teemo");
//打印被修改后的值
System.out.println(h.name);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
* Constructor:构造方法
* 创建对象:
* T newInstance(Object... initargs)
* 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
* Method:方法对象
* 执行方法:
* Object invoke(Object obj, Object... args)
* 获取方法名称:
* String getName:获取方法名
package test;
public class MethodTest {
public static void main(String[] args) {
HeroPlus h = new HeroPlus();
try {
// 获取这个名字叫做setName,参数类型是String的方法
Method m = h.getClass().getMethod("setName", String.class);
// 对h对象,调用这个方法
m.invoke(h, "盖伦");
// 使用传统的方式,调用getName方法
System.out.println(h.getName());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
9.Object类方法
- 1. getClass 方法
public final native Class<?> getClass();
final 方法、获取对象的运行时 class 对象,class 对象就是描述对象所属类的对象。这个方法通常是和 Java 反射机制搭配使用的
- 2. hashCode 方法
public native int hashCode();
该方法主要用于获取对象的散列值。Object 中该方法默认返回的是对象的堆内存地址。
- 3. equals 方法
public boolean equals(Object obj) { return (this == obj);}
该方法用于比较两个对象,如果这两个对象引用指向的是同一个对象,那么返回 true,否则返回 false。一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。
- 4. clone 方法
protected native Object clone() throws CloneNotSupportedException;
该方法是保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常。
默认的 clone 方法是浅拷贝。所谓浅拷贝,指的是对象内属性引用的对象只会拷贝引用地址,而不会将引用的对象重新分配内存。深拷贝则是会连引用的对象也重新创建。
- 5. toString 方法
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode());}
返回一个 String 对象,一般子类都有覆盖。默认返回格式如下:对象的 class 名称 + @ + hashCode 的十六进制字符串。
- 6. notify 方法
public final native void notify();
final 方法,主要用于唤醒在该对象上等待的某个线程。
- 7. notifyAll 方法
public final native void notifyAll();
final 方法,主要用于唤醒在该对象上等待的所有线程。
- 8. wait(long timeout) 方法
public final native void wait(long timeout) throws InterruptedException;
wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait() 方法一直等待,直到获得锁或者被中断。wait(long timeout) 设定一个超时间隔,如果在规定时间内没有获得锁就返回。
- 9. wait(long timeout, int nanos) 方法
public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}
wait(timeout);
}
- 参数说明
timeout:最大等待时间(毫秒)
nanos:附加时间在毫秒范围(0-999999)
该方法导致当前线程等待,直到其他线程调用此对象的 notify() 方法或notifyAll()方法,或在指定已经过去的时间。此方法类似于 wait 方法的一个参数,但它允许更好地控制的时间等待一个通知放弃之前的量。实时量,以毫微秒计算,计算公式如下:
1000000 * timeout + nanos
在所有其他方面,这种方法与 wait(long timeout) 做同样的事情。特别是 wait(0, 0) 表示和 wait(0) 相同。
-
- wait 方法
public final void wait() throws InterruptedException { wait(0);}
可以看到 wait() 方法实际上调用的是 wait(long timeout) 方法,只不过 timeout 为 0,即不等待。
- 11. finalize 方法
protected void finalize() throws Throwable { }
该方法是保护方法,主要用于在 GC 的时候再次被调用,如果我们实现了这个方法,对象可能在这个方法中再次复活,从而避免被 GC 回收。
10.深拷贝和浅拷贝
- 浅拷⻉:对基本数据类型进⾏值传递,对引⽤数据类型进⾏引⽤传递般的拷⻉,此为浅拷⻉。
- 深拷⻉:对基本数据类型进⾏值传递,对引⽤数据类型,创建⼀个新的对象,并复制其内容,此
为深拷⻉。
通过序列化和反序列化实现深拷贝
需要拷贝的类实现Serializable接口,否则会抛出NotSerializableException异常。
public class DeepCopy {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Category category = new Category(1);
Watch p1 = new Watch("high", category);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
//将p1序列化
oos.writeObject(p1);
oos.flush();
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Watch p2 = (Watch) ois.readObject();
System.out.println("修改前:");
System.out.println(p1.toString());
System.out.println(p2.toString());
System.out.println("修改后:");
p1.setLevel("low");
category.setCategory(2);
System.out.println(p1.toString());
System.out.println(p2.toString());
}
}
11.抽象类和接口
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
相同点
- 接口和抽象类都不能实例化
- 都位于继承的顶端,用于被其他实现或继承
- 都包含抽象方法,其子类都必须覆写这些抽象方法
不同点
参数 | 抽象类 | 接口 |
---|---|---|
声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected |
多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 |
备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。
现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
- 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
- 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。
Java异常
1.Java异常概述
1. Throwable
Throwable 是 Java 语言中所有错误与异常的超类。
Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。
2. Error(错误)
定义:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
特点:此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。
这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!
3. Exception(异常)
程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
运行时异常
定义:RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。
特点:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换异常、ArithmeticExecption算术异常。此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!
RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(就算我们没写异常捕获语句运行时也会抛出错误!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。
编译时异常
定义: Exception 中除 RuntimeException 及其子类之外的异常。
特点: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。
4. 受检异常与非受检异常
Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。
受检异常
编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。
非受检异常
编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。
2.异常处理总结
try 块: ⽤于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟⼀个finally 块。
catch 块: ⽤于处理 try 捕获到的异常。
finally 块: ⽆论是否捕获或处理异常,finally 块⾥的语句都会被执⾏。当在 try 块或catch 块中遇到 return 语句时,finally 语句块将在⽅法返回之前被执⾏。
在以下 4 种特殊情况下,finally 块不会被执⾏:
-
在 finally 语句块第⼀⾏发⽣了异常。 因为在其他⾏,finally 块还是会得到执⾏
-
在前⾯的代码中⽤了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句
之后,finally 会执⾏ -
程序所在的线程死亡。
-
关闭 CPU。
设计模式
设计模式原则
原则 | 说明 |
---|---|
开闭原则 | OOP 最基础的原则,软件实体应该对扩展开放,对修改关闭。 |
单一职责原则 | 一个类、接口或方法只负责一个职责,降低代码变更风险。 |
依赖倒置原则 | 程序应该依赖于抽象类或接口,而不是实现类。 |
接口隔离原则 | 将不同功能定义在不同接口,避免类依赖它不需要的接口,减少接口冗余。 |
里氏替换原则 | 开闭原则的补充,规定任何父类可以出现的地方子类都一定可以出现,约束继承泛滥。 |
迪米特原则 | 每个模块对其他模块都要尽可能少地了解和依赖,降低耦合。 |
合成/聚合原则 | 尽量使用组合(has-a)/聚合(contains-a)而不是继承(is-a)实现复用,避免方法污染和方法爆炸。 |
设计模式的分类
类型 | 说明 |
---|---|
创建型 | 创建对象时隐藏创建逻辑,不直接实例化对象,包括工厂/抽象工厂/单例/建造者/原型模式。 |
结构型 | 通过类和接口间的继承和引用创建复杂对象,包括适配器/桥接/过滤器/组合/装饰器/外观/享元/代理模式。 |
行为型 | 通过类的通信实现不同行为,包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式。 |
简单工厂模式
概念:由一个工厂对象创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。
场景:适用于工厂类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则。
举例:
- Calendar 类的
getInstance
方法,调用createCalendar
方法根据不同的地区参数创建不同的日历对象。 - Spring 中的 BeanFactory,根据传入一个唯一的标识来获得 Bean 实例。
工厂方法模式
概念:定义一个创建对象的接口,让接口的实现类决定创建哪种对象,让类的实例化推迟到子类中进行。
场景:主要解决了产品扩展的问题,在简单工厂模式中如果产品种类变多,工厂的职责会越来越多,不便于维护。
举例:
- Collection 接口中定义了一个抽象的
iterator
工厂方法,返回一个 Iterator 类的抽象产品。该方法通过 ArrayList 、HashMap 等具体工厂实现,返回 Itr、KeyIterator 等具体产品。 - Spring 的 FactoryBean 接口的
getObject
方法。
抽象工厂模式
概念:提供一个创建一系列相关对象的接口,无需指定它们的具体类。缺点是不方便扩展产品族,并且增加了系统的抽象性和理解难度。
场景:主要用于系统的产品有多于一个的产品族,而系统只消费其中某一个产品族产品的情况。
举例:java.sql.Connection 接口就是一个抽象工厂,其中包括很多抽象产品如 Statement、Blob、Savepoint 等。
单例模式
在任何情况下都只存在一个实例,构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。
优点是内存中只有一个实例,减少了开销;缺点是没有抽象层,难以扩展,与单一职责原则冲突。
举例:Spring 的 ApplicationContext 创建的 Bean 实例都是单例对象,还有 ServletContext、数据库连接池等也都是单例模式。
**饿汉式:**类加载时就创建单例对象,线程安全,但不管是否使用都创建对象可能会浪费内存。
// 饿汉式单例
public class Singleton{
// 指向自己实例的私有静态引用,主动创建
private static Singleton singleton = new Singleton();
// 私有的构造方法
private Singleton1(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton getSingleton(){
return singleton;
}
}
**懒汉式:**在外部调用时才会加载,线程不安全,可以加锁保证线程安全但效率低。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
**双重检查锁:**使用 volatile 以及多重检查来减小锁范围,提升效率。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton();
这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句:第一个 if 语句用来避免 uniqueInstance 已经被实例化之后的加锁操作,而第二个 if 语句进行了加锁,所以只能有一个线程进入,就不会出现 uniqueInstance == null 时两个线程同时进行实例化操作。
if (uniqueInstance == null) {
synchronized (Singleton.class) {
uniqueInstance = new Singleton();
}
}
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
**静态内部类:**同时解决饿汉式的内存浪费问题和懒汉式的线程安全问题。
当 Singleton 类被加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance()
方法从而触发 SingletonHolder.INSTANCE
时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。
这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举:《Effective Java》提倡的方式,不仅能避免线程安全问题,还能防止反序列化重新创建新的对象,也能防止反射破解单例的问题。
public enum Singleton {
INSTANCE;
private String objName;
public String getObjName() {
return objName;
}
public void setObjName(String objName) {
this.objName = objName;
}
public static void main(String[] args) {
// 单例测试
Singleton firstSingleton = Singleton.INSTANCE;
firstSingleton.setObjName("firstName");
System.out.println(firstSingleton.getObjName());
Singleton secondSingleton = Singleton.INSTANCE;
secondSingleton.setObjName("secondName");
System.out.println(firstSingleton.getObjName());
System.out.println(secondSingleton.getObjName());
// 反射获取实例测试
try {
Singleton[] enumConstants = Singleton.class.getEnumConstants();
for (Singleton enumConstant : enumConstants) {
System.out.println(enumConstant.getObjName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
firstName
secondName
secondName
secondName
代理模式
代理模式属于结构型模式,为其他对象提供一种代理来控制对该对象的访问。优点是可以增强目标对象的功能,降低代码耦合度;缺点是请求处理速度变慢,增加系统复杂度。
**静态代理:**代理对象持有被代理对象的引用,调用代理对象方法时会调用被代理对象的方法,但是会增加其他逻辑。需要手动完成,在程序运行前就已经存在代理类的字节码文件,代理类和被代理类的关系在运行前就已确定。 缺点是一个代理类只能为一个目标服务。
**动态代理:**动态代理在程序运行时通过反射创建具体的代理类,代理类和被代理类的关系在运行前是不确定的。动态代理的适用性更强,主要分为 JDK 动态代理和 CGLib 动态代理。
-
JDK 代理:
通过 Proxy 的newProxyInstance
方法获得代理对象,需要三个参数:被代理类的接口、类加载器以及 InvocationHandler 对象,需要重写 InvocationHandler 接口的 invoke 方法指明代理逻辑。 -
CGLib 代理:
通过 Enhancer 对象的create
方法获取代理对象,需要通过setSuperclass
方法设置代理类,以及setCallback
方法指明代理逻辑(传入一个MethodInterceptor 接口的实现类,具体代理逻辑声明在intercept
方法)。JDK 动态代理直接写字节码,而 CGLib 动态代理使用 ASM 框架写字节码, JDK 代理调用代理方法通过反射实现,而 GCLib 通过 FastClass 机制实现,为代理类和被代理类各生成一个类,该类为代理类和被代理类的方法分配一个 int 参数,调用方法时可以直接定位,效率更高。
装饰器模式
概念:在不改变原有对象的基础上将功能附加到对象,相比继承更加灵活。
场景:在不想增加很多子类的前提下扩展一个类的功能。
举例:java.io 包中,InputStream 通过 BufferedInputStream 增强为缓冲字节输入流。
和代理模式的区别:装饰器模式的关注点在于给对象动态添加方法,而动态代理更注重对象的访问控制。动态代理通常会在代理类中创建被代理对象的实例,而装饰器模式会将被装饰者作为构造方法的参数。
适配器模式
概念:作为两个不兼容接口之间的桥梁,使原本由于接口不兼容而不能一起工作的类可以一起工作。 缺点是过多使用适配器会让系统非常混乱,不易整体把握。
举例:
- java.io 包中,InputStream 通过 InputStreamReader 转换为 Reader 字符输入流。
- Spring MVC 中的 HandlerAdapter,由于 handler 有很多种形式,包括 Controller、HttpRequestHandler、Servlet 等,但调用方式又是确定的,因此需要适配器来进行处理,根据适配规则调用 handle 方法。
- Arrays.asList 方法,将数组转换为对应的集合(不能使用修改集合的方法,因为返回的 ArrayList 是 Arrays 的一个内部类)。
和装饰器模式的区别:适配器模式没有层级关系,适配器和被适配者没有必然连续,满足 has-a 的关系,解决不兼容的问题,是一种后置考虑;装饰器模式具有层级关系,装饰器与被装饰者实现同一个接口,满足 is-a 的关系,注重覆盖和扩展,是一种前置考虑。
和代理模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
策略模式
概念:定义了一系列算法并封装,之间可以互相替换。优点是算法可以自由切换,可以避免使用多重条件判断并且扩展性良好,缺点是策略类会增多并且所有策略类都需要对外暴露。
场景:主要解决在有多种算法相似的情况下,使用 if/else 所带来的难以维护。
举例:
- 集合框架中常用的 Comparator 就是一个抽象策略,一个类通过实现该接口并重写 compare 方法成为具体策略类。
- 线程池的拒绝策略。
模板模式
概念:使子类可以在不改变算法结构的情况下重新定义算法的某些步骤。优点是可以封装固定不变的部分,扩展可变的部分;缺点是每一个不同实现都需要一个子类维护,会增加类的数量。
场景:适用于抽取子类重复代码到公共父类。
举例:HttpServlet 定义了一套处理 HTTP 请求的模板,service 方法为模板方法,定义了处理HTTP请求的基本流程,doXXX 等方法为基本方法,根据请求方法的类型做相应的处理,子类可重写这些方法。
观察者模式
概念:也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。缺点是如果被观察者对象有很多的直接和间接观察者的话通知很耗时, 如果存在循环依赖的话可能导致系统崩溃,另外观察者无法知道目标对象具体是怎么发生变化的。
场景:主要解决一个对象状态改变给其他对象通知的问题。
定的,因此需要适配器来进行处理,根据适配规则调用 handle 方法。
- Arrays.asList 方法,将数组转换为对应的集合(不能使用修改集合的方法,因为返回的 ArrayList 是 Arrays 的一个内部类)。
和装饰器模式的区别:适配器模式没有层级关系,适配器和被适配者没有必然连续,满足 has-a 的关系,解决不兼容的问题,是一种后置考虑;装饰器模式具有层级关系,装饰器与被装饰者实现同一个接口,满足 is-a 的关系,注重覆盖和扩展,是一种前置考虑。
和代理模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
策略模式
概念:定义了一系列算法并封装,之间可以互相替换。优点是算法可以自由切换,可以避免使用多重条件判断并且扩展性良好,缺点是策略类会增多并且所有策略类都需要对外暴露。
场景:主要解决在有多种算法相似的情况下,使用 if/else 所带来的难以维护。
举例:
- 集合框架中常用的 Comparator 就是一个抽象策略,一个类通过实现该接口并重写 compare 方法成为具体策略类。
- 线程池的拒绝策略。
模板模式
概念:使子类可以在不改变算法结构的情况下重新定义算法的某些步骤。优点是可以封装固定不变的部分,扩展可变的部分;缺点是每一个不同实现都需要一个子类维护,会增加类的数量。
场景:适用于抽取子类重复代码到公共父类。
举例:HttpServlet 定义了一套处理 HTTP 请求的模板,service 方法为模板方法,定义了处理HTTP请求的基本流程,doXXX 等方法为基本方法,根据请求方法的类型做相应的处理,子类可重写这些方法。
观察者模式
概念:也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。缺点是如果被观察者对象有很多的直接和间接观察者的话通知很耗时, 如果存在循环依赖的话可能导致系统崩溃,另外观察者无法知道目标对象具体是怎么发生变化的。
场景:主要解决一个对象状态改变给其他对象通知的问题。
举例:ServletContextListener 能够监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用。当 Servlet 容器启动 Web 应用时调用 contextInitialized
方法,终止时调用 contextDestroyed
方法。