大学生初踏校门必备的26道Java基础面试题(附题解)
????Java基础面试题
该面试题中不涉及集合与并发编程内容,我会将这两部分内容划分成两个板块讲解!
由于我的误操作,将GitHub中Maven仓库上传了IDEA的那个jar文件,而导致GitHub仓库封禁,现在正向官方发邮件补救,将来我会重新建立仓库,大家等我好消息!
也提醒大家,一定要有版权意识!!!(虽然我不是故意的吧…)
目前很忙,博客也更新的很慢,还请大家谅解…
声明:以下面试都很简单,只是Java基础内容,而且不涉及并发编程、JVM、集合内容,这些我都会在后面的时间内,补充在后几篇文章内!Thanks!
1. 面试官:看你大学中也学过C语言是吧!你来说说C语言和Java语言有什么不同和优缺点!
C语言:面向过程编程、支持多重继承、内存需要程序员自己释放和垃圾回收,提供了灵活性(对资深程序员来说)
- 优点:面向过程编程比面向对象编程性能要高,因为类调用的时候需要实例化,性能开销比较大,比较消耗资源。比如:单片机、嵌入式开发、Linux/Unix等一般都使用面向过程编程,因为它们在性能方面需求更大,这也是C和C++语言多用于与硬件交互的原因!
- 缺点:没有面向对象编程那么易维护、易复用、易扩展
Java语言:面向对象编程、不提供指针来直接访问内存,程序内存更加安全、类是单继承(虽然Java的类不可以多继承,但是接口可以多继承)、有自动内存管理机制,不需要程序员手动释放无用内存
- 优点:由于面向对象有封装、继承、多态等特性,可以设计低耦合的系统,使系统更加灵活、更加易于维护和复用。因为它们的这些优点,所以Java语言多用于开发系统软件!
- 缺点:性能比面向过程编程低、开销大
2. 面试官:Java语言有哪些特点?
- 简单易学、入门学习的成本低
- 面向对象(封装继承多态)
- 跨平台
- 可靠性
- 安全性
- 支持多线程(C++语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而Java 语言却提供了多线程支持)
- 支持网络编程通讯(Java语言诞生本身就是为简化网络编程设计的,因此Java语言不仅支持网络编程而且很方便)
- 编译与解释并存
- 有丰富的社区和第三方核心类库等
3. 面试官:说说你对面向对象的理解
首先,面向对象即为OOP,就是将我们的程序模块化,对象化。对象在程序中具有
状态、行为和标识
,这意味着每个对象都可以拥有存储自己的内部数据(对象的状态)和方法(产生的行为)。即在内存中都有一块属于自己且唯一的空间——地址
。所谓“万物皆对象”,我们可以将对象视为一个奇特的变量,使用程序中的这些对象并为它们赋予属性和方法来模拟现实世界解决现实问题。
面向对象程序设计的,就认为它是一个抽象过程。在Java中,面向对象编程提供了几大特性,即封装、继承、多态。这些特性足以让我们在面向对象编程的时候,灵活运用并处理问题!
面向对象的特点:
- (1)万物皆对象。将对象视为奇特的变量,我们模拟现实世界,即所有的世间万物都皆是对象。都可以将其表示为程序中的对象。
- (2)程序是对象的集合,它们通过发送消息来告知彼此所需要的。即计算机发送的指令,可以把消息想象为对某个对象的方法的调用请求。
- (3)每个对象都有自己的由其他对象所构成的存储。换句话说,可以通过创建包含现有对象的包的方式创建新类型对象。因此,可以在程序中构建复杂的程序体系,同时将复杂性隐藏在对象的简单性背后。
- (4)每个对象都拥有其类型。在编程中,每个对象都是某个类(class)的一个实例,而类与类不同,都具有他们各自的特性和行为。
- (5)某一特定类型的所有对象都可以接收同样的消息。比如说,“几何形”肯定能接收一个正方形的消息,而也能接收圆形的消息。这意味着编写出来的几何形,可以交互和处理所有与几何形相关事物的代码。这种可替代性是OOP中最强有力的概念之一!
注意: 解释完这些后,如果面试官没有喊停就可以举一个例子,比如学生与教室(学生类与集合/数组)等。如果他问你封装、继承、多态,请参考第四题内容!
4. 面试官:说说你对封装、继承、多态的理解
- 封装: 尽可能隐藏对象的内部实现细节,控制对象的修改及访问权限,即:访问修饰符有private、default、protect、public
- 继承: 有点类似与我们生物学上的遗传,就是子类的一些特征是来源于父类的,儿子遗传了父亲或母亲的一些性格,或者相貌,又或者是运动天赋。面向对象里的继承也就是父类的相关的属性,可以被子类重复使用,子类不必再在自己的类里面重新定义一次,父类里有点我们只要拿过来用就好了。而对于自己类里面需要用到的新的属性和方法,子类就可以自己来扩展了。
- 多态: 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发 出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变 量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中 实现的方法,必须在由程序运行期间才能决定。在Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接 口(实现接口并覆盖接口中同一方法)。
5. 面试官:构造器(Constructor)是否可以被Override
在继承中,私有属性、方法和构造器并不能被继承,所以构造器也不能被Override(重写),但支持Overload(重载),所以可以看到一个类有多个构造器的情况!
6. 面试官:说说Override(重写)与Overload(重载)的区别吧
Override(重写): 当父类提供的方法无法满足子类需求时,可在子类中定义和父类相同的方法进行重写(Override)。其中规定方法名、参数列表必须相同,返回值范围小于等于父 类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类 方法访问修饰符为private则子类就不能重写该方法。
Overload(重载): 同一个类中定义多个相同名称的方法。其中规定方法名必须相同,参数类型不同、个数不同、顺序不同,与方法返回值和访问修饰符无关,发生在编译期。
7. 面试官:Java中自动装箱和拆箱你了解吗?
自动装箱:将基本数据类型用它们对应的引用类型包装起来
自动拆箱:将包装类转换为基本数据类型
注意: 如果他问你Java中基本数据类型和引用数据类型分别都有什么?分别是多少个字节?请参考第八题!
8. 面试官:Java中基本数据类型和引用数据类型分别都有什么?分别是多少个字节?
基本数据类型
整数:byte(1字节)、short(2字节)、int(4字节)、long(8字节)
小数:float(4字节)、double(8字节)
布尔:boolean(1字节)【单个的boolean 类型变量在编译的时候是使用的int 类型。所以JVM编译后boolean是4个字节!】
字符:char(2字节)
注意: 如果你的JVM基础不是那么好,请忽略boolean在编译器字节数的说法,记住不要自己坑自己!
9. 面试官:在一个静态方法内调用一个非静态成员为什么是 非法的?
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非 静态变量,也不可以访问非静态变量成员。
10. 面试官:说说接口和抽象类的区别
- 接口的方法默认是public修饰的,所有方法在接口中不能有任何实现(忽略Java8中添加了可以实现的新特性),抽象类可以有非抽象的方法。
- 接口中的实例变量 默认是final修饰的,而抽象类不一定。
- 一个类可以实现多个接口,但最多只能实现一个抽象类。
- 一个类实现接口的话要实现接口的所有方法,但抽象类不一定。
- 接口不能用new实例化,但可以声明,前提是必须引用一个实现该接口的对象。从设计层面来说,抽象类是对类的抽象,是一种模板设计。而接口是行为的抽象,是一种行为的规范与准则。
11. 面试官:聊聊成员变量与局部变量的区别有哪些
- 从语法形式上看,成员变量是属于类层面上定义的属性,而局部变量是属于方法层面定义的变量或是方法的参数。
- 成员变量可以被public、private、static等修饰符修饰,而局部变量不能被访问修饰符及static所修饰,但是成员变量和局部变量都可以被final所修饰。
- 从变量在内存中的存储方式来看,成员变量属于对象的一部分,而对象是存储在堆内存中的,局部变量是存储在栈内存中的(此处指的是虚拟机栈,而不是本地方法栈,不要混淆)
- 从变量在内存中的生存时间来看,成员变量是对象的一部分,它随着对象的创建而创建,随着对象的销毁而销毁。而局部变量随着方法的调用而自动消失(栈帧将局部变量压栈,调用时局部变量出栈)。
- 成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情 况例外被final修饰的成员变量也必须显示地赋值),而局部变量则不会自动赋值
12. 面试官:什么是方法的返回值?返回值在类的方法里的作用是什么?
方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果(前提是该方法能产生结果)。
返回值的作用:接收出结果,使得它可以用于其他的操作
13. 面试官:一个类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能正确执行吗 ?为什么?
主要作用是完成对类对象的初始化工作。
可以执行。
因为一个类即使没有声明 构造方法也会有默认的不带参数的构造方法。
14. 面试官:我们来聊聊静态方法和实例方法的不同点
- 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只可以使用"对象名.方法名"方式。也就是说,调用静态方法可以无需创建对象,而实例方法则需要创建对象喽!
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
15. 面试官:聊聊==和equals的关系与作用
==
:比较的是变量(栈)内存中存放对象(堆)的地址,用来判断两个两个对象的地址是否相同,也就是说是否指向同一个对象
- 简单来说,== 就是比较的地址是否相同(基本数据类型比较的是值,引用数据类型比较的是内存地址)
- 用于比较创建也就是实例化的对象地址,且必须是同一类型
- 比较的结果是true或false
- 还可用于比较两端的变量都代指的数字,比如说:账号假如是int型的,对账号输入的校验就可以用==来比较,因为两端的值都是指向堆中的
equals
:equals比较的是两个对象的内容,因为Object类中的equals的默认方法是public boolean equals(Object obj){},而所有的类都隐式的继承与Object类,所以适用于所有对象。如果没有对equals方法进行重写覆盖的话,而Object中的equals方法返回的却是==的判断(也就是比较的地址)
- 简单来说,equals比较的是内容,但是在没有覆盖Object中equals方法的同时,它比较的也是地址
- 用于比较字符串
- 也可以举同样的例子,比如:账号密码是String类型的话,我们就需要用equals来比较了
- 不同字符串的比较
- String s = “Hello”; 这种字符串定义是产生了一个对象存储在字符串池中的
- String s = new String(“Hello”); 虽然看起来都是Hello,但是地址也发生了变化,而这中定义方式,产生了两个对象,一个存储在字符串池中,而另一个存储在堆中
- 然后用equals比较这两种的时候那就是返回false了
注意:== 比equals的运行速度要快,因为只是比较的单纯地址
如果还不理解,请参考重写equals方法的例子,请点击链接阅读!
16. 面试官:你重写过 hashcode 和equals么,为什么重写equals时必须重写 hashCode 方法?
- hashCode方法的作用和特点
- hashCode方法的作用:是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
- hashCode方法的特点:散列表存储的是键值对(key-value),能根据“键”快速的检索出对应 的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
- 为什么要有hashCode
- 我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
- 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断 对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如 果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有 相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相 等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。 如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
hashCode与equals方法的规定
- 如果两个对象相等,则 hashcode 一定也是相同的
- 两个对象相等,对两个对象分别调用 equals 方法都返回 true
- 两个对象有相同的 hashcode 值,它们也不一定是相等的
- 因此,重写 equals 方法,则必须重写 hashCode 方法
- 注意: hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的地址和数据)
17. 面试官:说说你对final关键字的了解
final 关键字主要用在三个地方:变量、方法、类
- 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
- 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员 方法都会被隐式地指定为 final 方法。
- 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承 类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用 带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行 这些优化了)。类中所有的 private 方法都隐式地指定为 final。
18. 面试官:说说你对程序、线程、进程的理解,它们之间有什么关系
线程: 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的 过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空 间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工 作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序: 程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就 是说程序是静态的代码。
进程: 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说, 一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如:CPU时间片、内存空间、文件、输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执 行一个以上的程序段。
19. 面试官:线程的基本状态有哪几种?你知道它们的流程是怎么样的吗?
创建状态 -> 就绪状态 -> 运行状态 -> 阻塞状态 -> 死亡状态
线程的基本状态 |
---|
等待 |
阻塞 |
20. 面试官:你对Java中异常处理机制了解多少,可以和我聊聊吗?
Java异常类图 |
---|
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。
Throwable: 有两个重要的子类,即,Exception(异常) 和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误 (NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 异常由 Java 虚拟机抛出。
注意: 异常和错误的区别为 异常能被程序本身可以处理,而错误是无法处理。
21. 面试官:说说你开发中遇到的错误和异常有哪些?
温馨提示:以下异常选择几种即可,如果你的开发经验比较好,对栈内存溢出排查(*Error)和内存不足调优和参数设置等(OutOfMemoryException,简称OOM)比较擅长,完全可以拿出你的长处好好发挥。因为这里水比较深,如果不了解的并不建议你聊这两个!
Name | Desc |
---|---|
java.lang.NullPointerException(空指针异常) | 调用了未经初始化的对象或者是不存在的对象 |
java.lang.ClassNotFoundException(类文件未找到异常) | 指定的类不存在 |
java.lang.ClassCastException(类型转换异常) | 多态时,强制类型转换不匹配,不能转换 |
java.lang.NumberFormatException(数字转换异常) | 字符串转换为数字异常 |
java.lang.IndexOutOfBoundsException(数组下标越界异常) | 访问数组的下标超过了界限 |
java.lang.IllegalArgumentException(方法参数错误异常) | 方法调用时传递的参数发生了错误 |
java.lang.IllegalAccessException(权限不足异常) | 调用访问的时权限不满足 |
java.lang.ArithmeticException(算术异常) | 算术运算时出现的异常,比如除以0 |
java.lang.FileNotFoundException(文件未找到异常) | 试图打开一个文件,但找不到指定文件 |
java.lang.ArrayStoreException(数组存储异常) | 将不兼容类型的对象存入Object[]数组将引发 |
java.lang.NoSuchMethodException(方法不存在异常) | 反射读取访问,但指定的方法不存在 |
java.lang.EOFException(文件已结束异常) | 输入的过程中,文件或流提前关闭可导致 |
java.lang.InstantiationException(实例化异常) | 反射创建实例时,程序无法通过构造来创建时 |
java.lang.InterruptedException(被终止异常) | Thread的interrupt方法终止线程时抛出该异常 |
java.lang.CloneNotSupportedException(不支持克隆异常) | 不支持克隆方法时,调用clone()方法时抛出 |
java.lang.NoClassDefFoundException(未找到类定义异常) | JVM或类加载实例化类时,但找不到类的定义 |
java.lang.OutOfMemoryException(内存不足异常) | 当内存不足时,JVM继续分配对象时抛出 |
22. 面试官:Java中异常处理是怎么处理的?需要注意什么?
try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
catch 块:用于处理 try 捕获到的异常。
finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。 当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。这样保证了执行肯定会执行到finally块中的数据,常用于关闭资源等
在以下 4 种特殊情况下,finally 块不会被执行:
- 在 finally 语句块中发生了异常
- 在前面的代码中用了 System.exit()退出程序
- 程序所在的线程死亡
- 关闭CPU处理
需要注意的两个点:
- 尽量不要捕获类似Exception这样的通用异常,而是应该捕获特定异常,在这里是Thread.sleep()抛出的InterruptedException。
- 解释:我们有义务让自己的代码能够直观地体现出尽量多的信息,而泛泛 的Exception之类,恰恰隐藏了我们的目的。另外,我们也要保证程序不会捕获到我们不希望捕获的异常。比如,你可能更希望RuntimeException被扩散出来,而不是被捕获。
- 不要生吞(swallow)异常。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。
- 解释:生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的。如果我们不把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异 常。
- try-catch会引起很大的性能开销,不要随便使用异常进行流程控制
- 解释1:try-catch代码段会产生额外的性能开销,或者换个角度说,它往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的try包住整段的代码;与此同 时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
- 解释2:Java每实例化一个Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了
23. 面试官:说说final、finally和finalize的区别
首先,final、finally和finalize没有任何关系,只是在单词拼写上非常相似而已!
final:final用于声明属性、方法和类,分别表示属性不可变、方法不可重写(覆盖)和类不可继承
finally:finally表示异常处理结果的一部分,常与try块和catch块联用,finally块的内容表示不管有没有异常都会被执行,除非虚拟机停止工作或者执行
System.exit(1);
强制关闭finalize:finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。
24. 面试官:StringBuffer和StringBuilder的区别是什么?String为什么不可变?
简单的来说:String 类中使用 final 关键字字符数组保存字符串,
private final char value[]
,所以 String 对象是不可变的。而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中 也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以 这两种对象都是可变的。StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
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(线程安全)
25. 面试官:逻辑运算符&和&&的区别
&和&&都可用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式都为true时,整个运算符结果才为true。否则,只要有一方为false,则结果就为false。
&&作为短路与,即如果第一个表达式为false,则不再执行另一个表示式,而是直接返回false。
&作为位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作。(简单来说也就是机器码1和0之间的与关系)
26. 面试官:聊聊Java中Comparable和Comparator的区别
Comparable接口中可以实现一个compareTo方法来执行排序规则,主要用于创建对象的大小关系,该对象实现comparable接口,当
a.compareTo(b) > 0
时,则a>b;当a.compareTo(b) < 0
时,则a<b。Comparator接口中可以实现一个compare方法来进行排序,compare方法内主要靠定义compareTo规定的对象大小关系来确定对象的大小