引言:
Java学习基础知识
初始化
初始化一般遵循3个原则:
- 静态对象(变量)优先于非静态对象(变量)初始化,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次;
- 父类优先于子类进行初始化;
- 按照成员变量的定义顺序进行初始化。 即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化;
加载顺序
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
实例
1 | class Base { |
结果是:
1 | Base static block! |
深/浅拷贝
当拷贝一个变量时,原始引用和拷贝的引用指向同一个对象,改变一个引用所指向的对象会对另一个引用产生影响。
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所拷贝的对象,而不复制它所引用的对象。
1 | public class Person implements Cloneable { |
1 | public class Company implements Cloneable{ |
使用super(即Object)的clone方法只能进行浅拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
- 如果希望实现深拷贝,需要修改实现,比如修改为:
1 | public class Person implements Cloneable { |
- 另一种实现对象深拷贝的方式是序列化。
1 |
|
Java&C++
- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
- Java 支持自动垃圾回收,而 C++ 需要手动回收。(C++11 中引入智能指针,使用引用计数法垃圾回收)
- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
- Java 不支持操作符重载,虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
- Java 内置了线程的支持,而 C++ 需要依靠第三方库。
- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。
Object方法
equals()
1. equals() 与 == 的区别
- 对于基本类型,
- == 判断两个值是否相等,如:int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。
- 基本类型没有 equals() 方法。
- 对于引用类型,
- == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。
- equals() 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1,类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过 == 比较这两个对象。
- 情况2,类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。用来判断两个对象的内容是否相等。
1 | Integer x = new Integer(1); |
2. 等价关系
(一)自反性
1 | x.equals(x); // true |
(二)对称性
1 | x.equals(y) == y.equals(x); // true |
(三)传递性
1 | if (x.equals(y) && y.equals(z)) |
(四)一致性
多次调用 equals() 方法结果不变
1 | x.equals(y) == x.equals(y); // true |
(五)与 null 的比较
对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
1 | x.euqals(null); // false |
3. 实现
- 检查是否为同一个对象的引用,如果是直接返回 true;
- 检查是否是同一个类型,如果不是,直接返回 false;
- 将 Object 实例进行转型;
- 判断每个关键域是否相等。
1 | public class EqualExample { |
对比String里面的equals实现
1 | public boolean equals(Object anObject) { |
hashCode()
hasCode() 返回一个对象的散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定相同,但是散列值相同的两个实例不一定等价。
在覆盖 equals() 方法时必须覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。
下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。
1 | EqualExample e1 = new EqualExample(1, 1, 1); |
toString()
默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
1 | public class ToStringExample { |
clone()
1. cloneable
clone() 是 Object 的 protect 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
1 | public class CloneExample { |
重写 clone() 得到以下实现:
1 | public class CloneExample { |
1 | CloneExample e1 = new CloneExample(); |
以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。
1 | public class CloneExample implements Cloneable { |
总结:clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
反射
高分知乎回答
首先看一个在知乎上的优秀回答吧:
反射是什么呢?当我们的程序在运行时,需要动态的加载一些类这些类可能之前用不到所以不用加载到 JVM,而是在运行时根据需要才加载,这样的好处对于服务器来说不言而喻。
举个例子我们的项目底层有时是用 mysql,有时用 oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了,假设 com.java.dbtest.myqlConnection
,com.java.dbtest.oracleConnection
这两个类我们要用,这时候我们的程序就写得比较动态化,通过 Class tc = Class.forName(“com.java.dbtest.***Connection”);
通过类的全类名让 JVM 在服务器中找到并加载这个类,而如果是 Oracle 则传入的参数就变成另一个了。这时候就可以看到反射的好处了,这个动态性就体现出 Java 的特性了!
Spring中会发现当你配置各种各样的 bean 时,是以配置文件的形式配置的,你需要用到哪些 bean 就配哪些,spring 容器就会根据你的需求去动态加载,你的程序就能健壮地运行。
什么是反射
反射 (Reflection) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。通过 Class 获取 class 信息称之为反射(Reflection)
简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。
程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
Java 反射框架主要提供以下功能:
- 在运行时判断任意一个对象所属的类
- 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用 private 方法)
- 在运行时构造任意一个类的对象
- 在运行时调用任意一个对象的方法
重点:是运行时而不是编译时;(两判断一构造一调用)
主要用途
当我们在使用 IDE (如Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。
反射最重要的用途就是开发各种通用框架
很多框架(比如 Spring )都是配置化的(比如通过 XML 文件配置 JavaBean,Action 之类的),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。
获得Class对象
- 调用运行时类本身的
.class
属性
1 | Class clazz1 = Person.class; |
- 通过运行时类的对象获取
getClass();
1 | Person p = new Person(); |
- 使用 Class 类的
forName
静态方法
1 | public static Class<?> forName(String className) |
- (了解)通过类的加载器 ClassLoader
1 | ClassLoader classLoader = this.getClass().getClassLoader(); |
注解
什么是注解
Annontation 是 Java5 开始引入的新特征,中文名称叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation 像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。
Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation
包中。
简单来说:注解其实就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理。
为什么要用注解
传统的方式,我们是通过配置文件 .xml
来告诉类是如何运行的。
有了注解技术以后,我们就可以通过注解告诉类如何运行
例如:我们以前编写 Servlet 的时候,需要在 web.xml 文件配置具体的信息。我们使用了注解以后,可以直接在 Servlet 源代码上,增加注解后 Servlet 就被配置到 Tomcat 上了。也就是说,注解可以给类、方法上注入信息。
明显地可以看出,这样是非常直观的,并且 Servlet 规范是推崇这种配置方式的。
基本Annotation
在 java.lang 包下存在着5个基本的 Annotation,重点掌握前三个。
- @Override 重写注解
- @Deprecated 过时注解
- 该注解也非常常见,Java 在设计的时候,可能觉得某些方法设计得不好,为了兼容以前的程序,是不能直接把它抛弃的,于是就设置它为过时。
- Date对象中的 toLocalString() 就被设置成过时了
- 当我们在程序中调用它的时候,在 IDE 上会出现一条横杠,说明该方法是过时的。
1 |
|
- @SuppressWarnings 抑制编译器警告注解
- @SafeVarargs Java 7“堆污染”警告
- 什么是堆污染呢??当把一个不是泛型的集合赋值给一个带泛型的集合的时候,这种情况就很容易发生堆污染。
- 这个注解也是用来抑制编译器警告的注解,用的地方并不多。
- @FunctionalInterface 用来指定该接口是函数式接口
- 用该注解显式指定该接口是一个函数式接口。
自定义注解类编写规则
- Annotation 型定义为 @interface, 所有的 Annotation 会自动继承 java.lang.Annotation 这一接口,并且不能再去继承别的类或是接口.
- 参数成员只能用 public 或默认(default)这两个访问权修饰
- 参数成员只能用基本类型 byte,short,char,int,long,float,double,boolean 八种基本数据类型和 String、Enum、Class、annotations 等数据类型,以及这一些类型的数组
- 要获取类方法和字段的注解信息,必须通过 Java 的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法
- 注解也可以没有定义成员, 不过这样注解就没啥用了 PS:自定义注解需要使用到元注解
自定义注解实例
@Target({METHOD,TYPE}) 表示这个注解可以用用在类/接口上,还可以用在方法上
@Retention(RetentionPolicy.RUNTIME) 表示这是一个运行时注解,即运行起来之后,才获取注解中的相关信息,而不像基本注解如@Override
那种不用运行,在编译时idea就可以进行相关工作的编译时注解。
@Inherited 表示这个注解可以被子类继承
@Documented 表示当执行javadoc的时候,本注解会生成相关文档
1 | import java.lang.annotation.Documented; |
泛型
通俗解释
通俗的讲,泛型就是操作类型的 占位符,即:假设占位符为 T,那么此次声明的数据结构操作的数据类型为T类型。
假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?答案是可以使用 Java 泛型。
使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
泛型方法
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数 只能代表引用型类型,不能是原始类型 (像 int,double,char 的等)。
1 | public class GenericMethodTest |
泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
1 | public class Box<T> { |
类型通配符
- 类型通配符一般是使用
?
代替具体的类型参数。例如List
在逻辑上是List
,List
等所有 List<具体类型实参> 的父类。 - 类型通配符上限通过形如 List 来定义,如此定义就是通配符泛型值接受 Number 及其下层子类类型。
- 类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其三层父类类型,如 Objec 类型的实例。
异常
总体上异常分三类:
- 错误(Error)
指的是系统级别的异常,通常是内存用光了。在默认设置下,一般java程序启动的时候,最大可以使用16m的内存。如果不停的给StringBuffer追加字符,很快就把内存使用光了。抛出OutOfMemoryError
与运行时异常一样,错误也是不要求强制捕捉的
- 运行时异常(Exception )
运行时异常RuntimeException指: 不是必须进行try catch的异常
常见运行时异常:
- 除数不能为0异常:ArithmeticException
- 下标越界异常:ArrayIndexOutOfBoundsException
- 空指针异常:NullPointerException
在编写代码的时候,依然可以使用try catch throws进行处理,与可查异常不同之处在于,即便不进行try catch,也不会有编译错误。Java之所以会设计运行时异常的原因之一,是因为下标越界,空指针这些运行时异常太过于普遍,如果都需要进行捕捉,代码的可读性就会变得很糟糕。
- 可查异常(Exception )
可查异常即必须进行处理的异常,要么try catch住,要么往外抛,谁调用,谁处理,比如 FileNotFoundException
如果不处理,编译器,就不让你通过
运行时异常与非运行时异常的区别:
运行时异常是不可查异常,不需要进行显式的捕捉
非运行时异常是可查异常,必须进行显式的捕捉,或者抛出
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
- 受检异常 :需要用 try…catch… 语句捕获并进行处理,并且可以从异常中恢复;
- 非受检异常 :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。
Java参数传递
值传递(pass by value)是指在调用函数时将实际参数
复制
一份传递到函数中,这样在函数中如果对参数
进行修改,将不会影响到实际参数。引用传递(pass by reference)是指在调用函数时将实际参数的地址
直接
传递到函数中,那么在函数中对参数
所进行的修改,将会影响到实际参数。
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。
1 | public class Dog { |
1 | public class PassByValueExample { |
如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
1 | class PassByValueExample { |
Java继承
Java 面向对象的基本思想之一是封装细节并且公开接口。Java 语言采用访问控制修饰符来控制类及类的方法和变量的访问权限,从而向使用者暴露接口,但隐藏实现细节。访问控制分为四种级别:
修饰符 | 当前类 | 同 包 | 子 类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
- 类的成员不写访问修饰时默认为 default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。
- 受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。
接口和抽象类的区别
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
- 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。
- 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。
- 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
super
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
1 | public class SuperExample { |
1 | public class SuperExtendExample extends SuperExample { |
1 | SuperExample e = new SuperExtendExample(1, 2, 3); |
1 | SuperExample.func() |
- 实例化一个对象, 其构造方法会被调用
- 其父类的构造方法也会被调用,并且是父类构造方法先调用
- 子类构造方法会默认调用父类的无参的构造方法
当父类Hero提供了一个有参的构造方法,但是没有提供无参的构造方法,子类应该怎么处理?
1 | public class Hero { |
作为子类,无论如何都会调用父类的构造方法。
默认情况下,会调用父类的无参的构造方法。
但是,当父类没有无参构造方法的时候( 提供了有参构造方法,并且不显示提供无参构造方法),子类就会抛出异常,因为它尝试去调用父类的无参构造方法。
这个时候,必须通过super去调用父类声明的、存在的、有参的构造方法
1 | public class ADHero extends Hero implements AD{ |
在 Java 中定义一个不做事且没有参数的构造方法的作用
Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
使用 this 和 super 要注意的问题:
- 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。
- this、super不能用在static方法中。
简单解释一下:
被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西。
Java封装
匿名内部类
匿名内部类也就是没有名字的内部类。使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口
实例1:不使用匿名内部类来实现抽象方法
1 | abstract class Person { |
运行结果:eat something
可以看到,我们用 Child 继承了 Person 类,然后实现了 Child 的一个实例,将其向上转型为 Person 类的引用
但是,如果此处的 Child 类只使用一次,那么将其编写为独立的一个类岂不是很麻烦?这个时候就引入了匿名内部类
实例2:匿名内部类的基本实现
1 | abstract class Person { |
运行结果:eat something
可以看到,我们直接将抽象类 Person 中的方法在大括号中实现了,这样便可以省略一个类的书写,并且,匿名内部类还能用于接口上。
实例3:在接口上使用匿名内部类
1 | interface Person { |
运行结果:eat something
由上面的例子可以看出,只要一个类是抽象的或是一个接口,那么其子类中的方法都可以使用匿名内部类来实现
最常用的情况就是在多线程的实现上,因为要实现多线程必须继承 Thread 类或是继承 Runnable 接口
实例4:Thread类的匿名内部类实现
1 | public class Demo { |
运行结果:1 2 3 4 5
实例5:Runnable接口的匿名内部类实现
1 | public class Demo { |
运行结果:1 2 3 4 5
Java多态
多态的理解(多态的实现方式)
- 方法重载(overload):实现的是编译时的多态性(也称为前绑定)。
- 方法重写(override):实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西。
项目中对多态的应用
- 举一个简单的例子,在物流信息管理系统中,有两种用户:订购客户和卖房客户,两个客户都可以登录系统,他们有相同的方法 Login,但登陆之后他们会进入到不同的页面,也就是在登录的时候会有不同的操作,两种客户都继承父类的 Login 方法,但对于不同的对象,拥有不同的操作。
面相对象开发方式优点
- 较高的开发效率:可以把事物进行抽象,映射为开发的对象。
- 保证软件的鲁棒性:高重用性,可以重用已有的而且在相关领域经过长期测试的代码。
- 保证软件的高可维护性:代码的可读性非常好,设计模式也使得代码结构清晰,拓展性好。
向上/下转型
父类引用能指向子类对象,子类引用不能指向父类对象;
向上转型
父类引用指向子类对象,例如:
1 | Father f1 = new Son(); |
向下转型
把指向子类对象的父类引用赋给子类引用,需要强制转换,例如:
1 | Father f1 = new Son(); |
但有运行出错的情况:
1 | Father f2 = new Father(); |
在不确定父类引用是否指向子类对象时,可以用 instanceof 来判断:
1 | if(f3 instanceof Son){ |
instanceof
instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符。
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
1 | public class Main { |
数据类型&装/拆箱
- 4 类 8 种基本数据类型。4 整数型,2 浮点型,1 布尔型,1 字符型
类型 | 存储 | 取值范围 | 默认值 | 包装类 |
---|---|---|---|---|
整数型 | ||||
byte | 8 | 最大存储数据量是 255,最小 -2^7^,最大 2^7^-1, [-128~127] | (byte) 0 | Byte |
short | 16 | 最大数据存储量是 65536,[-2^15^,2^15^-1], [-32768,32767],±3万 | (short) 0 | Short |
int | 32 | 最大数据存储容量是 231-1, [-2^31^,2^31^-1],±21亿,[ -2147483648, 2147483647] | 0 | Integer |
long | 64 | 最大数据存储容量是 264-1, [-2^63^,2^63^-1], ±922亿亿(±(922+16个零)) | 0L | Long |
浮点型 | ||||
float | 32 | 数据范围在 3.4e-45~1.4e38,直接赋值时必须在数字后加上 f 或 F | 0.0f | Float |
double | 64 | 数据范围在 4.9e-324~1.8e308,赋值时可以加 d 或 D 也可以不加 | 0.0d | Double |
布尔型 | ||||
boolean | 1 | true / flase | false | Boolean |
字符型 | ||||
char | 16 | 存储 Unicode 码,用单引号赋值 | ‘\u0000’ (null) | Character |
- 引用数据类型
- 自动装箱和拆箱
1 | // jdk 1.5 |
上面的代码在 jdk1.4 以后的版本都不会报错,它实现了自动拆装箱的功能
ValueOf缓存池
new Integer(123) 与 Integer.valueOf(123) 的区别在于,new Integer(123) 每次都会新建一个对象,而 Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。
1 | Integer x = new Integer(123); |
编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
1 | Integer m = 123; |
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。
1 | // valueOf 源码实现 |
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
1 | static final int low = -128; |
Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
因此在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
Java运算符
位运算符
Java 定义了位运算符,应用于整数类型 (int),长整型 (long),短整型 (short),字符型 (char),和字节型 (byte)等类型。
下表列出了位运算符的基本运算,假设整数变量A的值为60和变量B的值为13
A(60):0011 1100
B(13):0000 1101
操作符 | 名称 | 描述 | 例子 |
---|---|---|---|
& | 与 | 如果相对应位都是 1,则结果为 1,否则为 0 | (A&B)得到 12,即 0000 1100 |
| | 或 | 如果相对应位都是 0,则结果为 0,否则为 1 | (A|B)得到 61,即 0011 1101 |
^ | 异或 | 如果相对应位值相同,则结果为 0,否则为 1 | (A^B)得到49,即 0011 0001 |
〜 | 非 | 按位取反运算符翻转操作数的每一位,即 0 变成 1,1 变成 0 | (〜A)得到-61,即1100 0011 |
<< | 左移 | (左移一位乘2)按位左移运算符。左操作数按位左移右操作数指定的位数。左移 n 位表示原来的值乘 2n | A << 2得到240,即 1111 0000 |
>> | (右移一位除2)有符号右移,按位右移运算符。左操作数按位右移右操作数指定的位数 | A >> 2得到15即 1111 | |
>>> | 无符号右移 | 无符号右移,按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充 | A>>>2得到15即0000 1111 |
& 和 && 、| 和 ||
(1)&& 和 & 都是表示与,区别是 && 只要第一个条件不满足,后面条件就不再判断。而 & 要对所有的条件都进行判断。
1 | // 例如: |
(2)|| 和 | 都是表示 “或”,区别是 || 只要满足第一个条件,后面的条件就不再判断,而 | 要对所有的条件进行判断。
1 | public static void main(String[] args) { |
Java字符串
String常量池
Java 中字符串对象创建有两种形式,一种为字面量形式,如 String str = "abc";
,另一种就是使用 new 这种标准的构造对象的方法,如 String str = new String("abc");
,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式。然而这两种实现其实存在着一些性能和内存占用的差别。这一切都是源于 JVM 为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池。
工作原理
当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。
1 | public class Test { |
1 | //只有在这两种情况下会将对象引用自动加入到常量池 |
String
String 被声明为 final,因此它不可被继承。
- 在 Java 8 中,String 内部使用 char 数组存储数据。
- 在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。
1 | public final class String implements java.io.Serializable, Comparable<String>, CharSequence { |
value 用 final 修饰,表示编译器不允许我把 value 指向堆区另一个地址。但如果我直接对数组元素动手,分分钟搞定。
1 | final int[] value={1,2,3}; |
或者更粗暴的反射直接改,也是可以的。
1 | final int[] array={1,2,3}; |
所以 String 是不可变,关键是因为 SUN 公司的工程师,在后面所有 String 的方法里很小心的没有去动 Array 里的元素,没有暴露内部成员字段。private final char value[]
这一句里,private 的私有访问权限的作用都比 final 大。而且设计师还很小心地把整个 String 设成 final 禁止继承,避免被其他人继承后破坏。所以 String 是不可变的关键都在底层的实现,而不是一个 final。
不可变的好处
- 可以缓存 hash 值
因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
- String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
- 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
- 线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
- 安全性代码示范
一个函数 appendStr()在不可变的 String 参数后面加上一段 “bbb” 后返回。appendSb( )负责在可变的 StringBuilder 后面加“bbb”。
1 | class Test{ |
如果程序员不小心像上面例子里,直接在传进来的参数上加 “bbb”,因为 Java 对象参数传的是引用,所以可变的的 StringBuffer 参数就被改变了。可以看到变量 sb 在 Test.appendSb(sb) 操作之后,就变成了 “aaabbb”。有的时候这可能不是程序员的本意。所以 String 不可变的安全性就体现在这里。
再看下面这个 HashSet 用 StringBuilder 做元素的场景,问题就更严重了,而且更隐蔽。
1 | class Test{ |
StringBuilder 型变量 sb1 和 sb2 分别指向了堆内的字面量 “aaa” 和 “aaabbb”。把他们都插入一个 HashSet。到这一步没问题,但如果后面我把变量 sb3 也指向 sb1 的地址,再改变 sb3 的值,因为 StringBuilder 没有不可变性的保护,sb3 直接在原先 “aaa” 的地址上改。导致 sb1 的值也变了。这时候,HashSet 上就出现了两个相等的键值 “aaabbb”。破坏了 HashSet 键值的唯一性。所以千万不要用可变类型做 HashMap 和 HashSet 键值。
在并发场景下,多个线程同时读一个资源,是不会引发竞争条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全。
同时String 另外一个字符串常量池的属性。这样在大量使用字符串的情况下,可以节省内存空间,提高效率。
substring
在jdk 6 中,当调用 substring 方法的时候,会创建一个新的string对象,但是这个string的值仍然指向堆中的同一个字符数组。这两个对象中只有count和offset 的值是不同的,字符串中包含的字符个数以及数组的第一个位置索引。
1 | //JDK 6 |
在jdk 7 中,substring方法会在堆内存中创建一个新的数组。
Java源码中关于这部分的主要代码如下:
1 | //JDK 7 |
以上是JDK 7中的subString方法,其使用new String
创建了一个新字符串,避免对老字符串的引用。从而解决了内存泄露问题。
String对于 + 的重载
- 会创建一个新的字符串;
- 编译时会将 + 转为StringBuilder的append方法。
- 注意新的字符串是在运行时在堆里创建的。
String str1 = “ABC”;
可能创建一个或者不创建对象,如果”ABC”这个字符串在java String池里不存在,会在java String池里创建一个创建一个String对象(“ABC”),然后str1指向这个内存地址,无论以后用这种方式创建多少个值为”ABC”的字符串对象,始终只有一个内存地址被分配,之后的都是String的拷贝,Java中称为“字符串驻留”,所有的字符串常量都会在编译之后自动地驻留。- 注意只有字符串常量是共享的,+ 和 substring等操作的结果不是共享的,substring也会在堆中重新创建字符串。
String拼接
所有的所谓字符串拼接,都是重新生成了一个新的字符串。
使用+
拼接字符串
在Java中,拼接字符串最简单的方式就是直接使用符号+
来拼接。如:
1 | String wechat = "Hollis"; |
这里要特别说明一点,有人把Java中使用+
拼接字符串的功能理解为运算符重载。这其实只是Java提供的一个语法糖。
注意:阿里巴巴Java开发手册中不建议在循环体中使用+
进行字符串拼接呢?
原代码:
1 | long t1 = System.currentTimeMillis(); |
反编译后代码如下:
1 | long t1 = System.currentTimeMillis(); |
我们可以看到,反编译后的代码,在for
循环中,每次都是new
了一个StringBuilder
,然后再把String
转成StringBuilder
,再进行append
,如下:
1 | int i = 5; |
而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。
所以,阿里巴巴Java开发手册建议:循环体内,字符串的连接方式,使用 StringBuilder
的 append
方法进行扩展。而不要使用+
。
1 | // 下面两行也相等,因为String.valueOf(i)也是调用Integer.toString(i)来实现的。 |
concat
除了使用+
拼接字符串之外,还可以使用String类中的方法concat方法来拼接字符串。如:
1 | String wechat = "Hollis"; |
StringBuffer
关于字符串,Java中除了定义了一个可以用来定义字符串常量的String
类以外,还提供了可以用来定义字符串变量的StringBuffer
类,它的对象是可以扩充和修改的。
使用StringBuffer
可以方便的对字符串进行拼接。如:
1 | StringBuffer wechat = new StringBuffer("Hollis"); |
StringBuilder
除了StringBuffer
以外,还有一个类StringBuilder
也可以使用,其用法和StringBuffer
类似。如:
1 | StringBuilder wechat = new StringBuilder("Hollis"); |
总结
本文介绍了什么是字符串拼接,虽然字符串是不可变的,但是还是可以通过新建字符串的方式来进行字符串的拼接。
常用的字符串拼接方式有五种,分别是使用+
、使用concat
、使用StringBuilder
、使用StringBuffer
以及使用StringUtils.join
。
由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。
因此,经过对比,我们发现,直接使用StringBuilder
的方式是效率最高的。因为StringBuilder
天生就是设计来定义可变字符串和字符串的变化操作的。
但是,还要强调的是:
1、如果不是在循环体中进行字符串拼接的话,直接使用+
就好了。
2、如果在并发场景中进行字符串拼接的话,要使用StringBuffer
来代替StringBuilder
。
String里面的equals
String重写了Object的hashCode和equals
1 | public boolean equals(Object anObject) { |
StringBuffer、StringBuilder、String
- 可变性
String 不可变
StringBuffer 和 StringBuilder 可变
- 线程安全
String 不可变,因此是线程安全的
StringBuilder 不是线程安全的
StringBuffer 是线程安全的,内部使用 synchronized 进行同步
- 性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
- 对于三者使用的总结:
操作少量的数据:适用String
单线程操作字符串缓冲区下操作大量数据:适用StringBuilder
多线程操作字符串缓冲区下操作大量数据:适用StringBuffer
- 代码
1 | public final class StringBuffer |
AbstractStringBuilder
1 | char[] value; |
扩容
1 | public void ensureCapacity(int minimumCapacity) { |
扩容的方法最终是由expandCapacity()
实现的,在这个方法中首先把容量扩大为原来的容量加2,如果此时仍小于指定的容量,那么就把新的容量设为minimumCapacity
。然后判断是否溢出,如果溢出了,把容量设为Integer.MAX_VALUE
。最后把value
值进行拷贝,这显然是耗时操作。
append()方法
1 | public AbstractStringBuilder append(String str) { |
append()
是最常用的方法,用于追加字符串。如果str
是null
,则会调用appendNull()
方法,会追加'n'
、'u'
、'l'
、'l'
这几个字符。如果不是null
,则首先扩容,然后调用String
的getChars()
方法将str
追加到value
末尾。最后返回对象本身,所以append()
可以连续调用。
StringBuilder
AbstractStringBuilder
已经实现了大部分需要的方法,StringBuilder
和StringBuffer
只需要调用即可。
append()方法
1 | public StringBuilder append(String str) { |
toString()
1 | public String toString() { |
toString()
方法返回了一个新的String
对象,与原来的对象不共享内存。
SringBuffer
StiringBuffer
跟StringBuilder
类似,只不过为了实现同步,很多方法使用lSynchronized
修饰
1 | public synchronized int length() { |
可以看到,方法前面确实加了Synchronized
。另外,在上面的append()
以及setLength()
方法里面还有个变量toStringCache
。这个变量是用于最近一次toString()
方法的缓存,任何时候只要StringBuffer
被修改了这个变量会被赋值为null
。StringBuffer
的toString
如下:
1 | public synchronized String toString() { |
在这个方法中,如果toStringCache
为null
则先缓存。最终返回的String
对象有点不同,这个构造方法还有个参数true
。找到String
的源码看一下:
1 | String(char[] value, boolean share) { |
原来这个构造方法构造出来的String
对象并没有实际复制字符串,只是把value
指向了构造参数,这是为了节省复制元素的时间。不过这个构造器是具有包访问权限,一般情况下是不能调用的。
Java内存模型
并发编程需要解决的问题
线程间通信&线程间同步
线程之间通信机制分为两种:共享内存、消息传递
共享内存通信与同步
操作类型 | 实现方式 |
---|---|
通信 | 线程之间共享程序的公共状态,通过写-读内存中的变量的公共状态进行隐式通信 |
同步 | 显式进行同步,必须显式制定某个方法或某段代码需要在线程之间互斥执行 |
消息传递通信与同步
操作类型 | 实现方式 |
---|---|
通信 | 线程之间没有公共状态,线程之间通过发送消息显式进行通信 |
同步 | 隐式进行同步,消息发送必须在消息发送之前 |
注意:java并发采用的是共享内存模型,java线程之间的通信总是隐式进行的。
内存模型概念
在Java中所有的实例对象、静态数据域、和数组元素都存储在堆内存当中,堆内存在线程之间是共享的。 ——堆中数据域是线程共享的
局部变量、方法定义参数、和异常处理器参数不会在线程之间共享、他们不会有内存可见性问题,也不受内存模型的影响。——线程独享的
JMM简介
JMM决定一个线程对共享变量的写入何时对另一个线程可见。(可见性保证)
JMM同步规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
- 由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量的储存在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须都工作内存进行看。
- 首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
- 内存模型图
并发关键字
volatile
是 Java 虚拟机提供的轻量级的同步机制
- 保证可见性
- 禁止指令排序
- 不保证原子性
三大特性
可见性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class VolatileDemo {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " coming...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.addOne();
System.out.println(Thread.currentThread().getName() + " updated...");
}).start();
while (data.a == 0) {
// looping
}
System.out.println(Thread.currentThread().getName() + " job is done...");
}
}
class Data {
// int a = 0;
volatile int a = 0;
void addOne() {
this.a += 1;
}
}
// 新线程修改a值后,可通知main线程,即可见性如果不加 volatile 关键字,则主线程会进入死循环,加 volatile 则主线程能够退出,说明加了 volatile 关键字变量,当有一个线程修改了值,会马上被另一个线程感知到,当前值作废,从新从主内存中获取值。对其他线程可见,这就叫可见性。
原子性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class VolatileDemo {
public static void main(String[] args) {
// test01();
test02();
}
// 测试原子性
private static void test02() {
Data data = new Data();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
data.addOne();
}
}).start();
}
// 默认有 main 线程和 gc 线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(data.a);
}
}
class Data {
volatile int a = 0;
void addOne() {
this.a += 1;
}
}发现并不能输入 20000
禁止指令排序
volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,他的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)
由于编译器个处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能个这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
线程安全性保证
- 工作内存与主内存同步延迟现象导致可见性问题
- 可以使用 synchronzied 或 volatile 关键字解决,它们可以使用一个线程修改后的变量立即对其他线程可见
- 对于指令重排导致可见性问题和有序性问题
- 可以利用 volatile 关键字解决,因为 volatile 的另一个作用就是禁止指令重排序优化
volatile的应用
单例
多线程环境下可能存在的安全问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton01 {
private static Singleton01 instance = null;
private Singleton01() {
System.out.println(Thread.currentThread().getName() + " construction...");
}
public static Singleton01 getInstance() {
if (instance == null) {
instance = new Singleton01();
}
return instance;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.execute(()-> Singleton01.getInstance());
}
executorService.shutdown();
}
}发现构造器里的内容会多次输出
双重锁单例
- 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class Singleton02 {
private static volatile Singleton02 instance = null;
private Singleton02() {
System.out.println(Thread.currentThread().getName() + " construction...");
}
public static Singleton02 getInstance() {
if (instance == null) {
synchronized (Singleton01.class) {
if (instance == null) {
instance = new Singleton02();
}
}
}
return instance;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.execute(()-> Singleton02.getInstance());
}
executorService.shutdown();
}
}如果没有加 volatile 就不一定是线程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。
原因是在于某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。
instance = new Singleton()
可以分为以下三步完成1
2
3memory = allocate(); // 1.分配对象空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null步骤 2 和步骤 3 不存在依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种优化是允许的。
发生重排
1
2
3memory = allocate(); // 1.分配对象空间
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null,但对象还没有初始化完成
instance(memory); // 2.初始化对象所以不加 volatile 返回的实例不为空,但可能是未初始化的实例
SpringBoot与SSM
首先看搭建 SSM 框架时,我们需要哪些步骤
加相关的 jar 包
配置 web.xml,加载 Spring,SpringMVC
配置数据库连接,spring 事务
配置加载配置文件的读取,开启注解
配置日志文件
配置完成,部署 tomcat 调试
而这些配置 SpringBoot 都帮我们做好了:
就是这些 starter 依赖,帮我们做了很多配置。
springboot 框架使用 starter 依赖主要帮我们做了两点:
1.引入相关的 jar
2.自动完成 bean 配置。
总结:
- Springboot 将原有的 xml 配置,简化为 java 注解
- 使用 IDE 可以很方便的搭建一个 springboot 项目,选择对应的 maven 依赖,简化Spring应用的初始搭建以及开发过程
- springboot 有内置的 tomcat 服务器,可以 jar 形式启动一个服务,可以快速部署发布 web 服务
- springboot 使用 starter 依赖自动完成 bean 配置,,解决 bean 之间的冲突,并引入相关的 jar 包(这一点最重要)
Comparable&Comparator
Comparable 是排序接口
若一个类实现了Comparable接口,就意味着“该类支持排序”。 即实现Comparable接口的类支持排序,实现Comparable接口的类的对象的List列表(或数组),该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序。
在用Collections类的sort方法排序时若不指定Comparator,那就以自然顺序排序。所谓自然顺序就是实现Comparable接口中按照 compareTo 方法中的规则进行。
若一个类实现了comparable接口,则意味着该类支持排序。如String、Integer自己就实现了Comparable接口,可完成比较大小操作。
实际上所有实现了 Comparable 接口的 Java 核心类的结果都和 equlas 方法保持一致。
实现了 Comparable 接口的对象才能够直接被用作 SortedMap (SortedSet) 的 key,要不然得在外边指定 Comparator 排序规则。因此自己定义的类如果想要使用有序的集合类,需要实现 Comparable 接口。
1 | package java.lang; |
1 | public class BookBean implements Serializable, Comparable { |
Comparator 是比较器接口
我们若需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口);那么,我们可以建立一个“该类的比较器”来进行排序。这个“比较器”只需要实现Comparator接口即可。
也就是说,我们可以通过“实现Comparator类来新建一个比较器”,然后通过该比较器对类进行排序。Comparator体现了一种策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。
1 | package java.util; |
注:若一个类要实现Comparator接口,它一定要实现compareTo(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数。因为任何类,默认都是已经实现了equals(Object obj)的。
二者比较
Comparable是排序接口;若一个类实现了Comparable接口,就意味着“该类支持排序”。
而Comparator是比较器;我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
不难发现:Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
1 | package compar; |