知识点-1

1. Java 基础

Java 入门(基础概念与常识)

Java 语言有哪些特点?

1. 简单易学;
  1. 面向对象(封装,继承,多态);
  2. 平台无关性( Java 虚拟机实现平台无关性);
  3. 可靠性;
  4. 安全性;
  5. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程⽀持);
  6. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程并且很方便);
  7. 编译与解释并存;

关于 JVM、JDK 和 JRE

JVM:

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,MacOS),目的是使用相同的字节码,它们都会给出相同的结果。

什么是字节码?采用字节码的好处是什么?

在 Java 中,JVM 可以理解的代码就叫做字节码 (即扩展名为.class的⽂件),它不面向任何特定的处理器,只面向虚拟机。Java 通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较⾼效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

Java 程序从源代码到运行一般有下面 3 步:

总结:

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

JDK 和 JRE

JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。

如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。

Oracle JDK 和 OpenJDK 的区别

两者非常接近

  • Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。

  • 对于 Java 7,没什么关键的地方。OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle 工程师维护。

  • Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。

import java 和 javax 有什么区别?

刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。
所以,实际上 java 和 javax 没有区别。这都是一个名字。

为什么说 Java 语言“编译与解释并存”?

高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读, 有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。

Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。

Java 语法

标识符和关键字的区别是什么?

在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符,简单来说,标识符就是一个名字。但是有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符。比如,在我们的日常生活中 ,“警察局”这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫“警察局”,“警察局”就是我们日常生活中的关键字。

访问控制 private protected public
类,方法和变量修饰符 abstract class extends final implements
new static interface synchronized transient
native
程序控制 break continue return do while
for instanceof switch case default
if else
错误处理 try catch throw throws finally
包相关 import package
基本类型 boolean byte char double float
long int short
变量引用 super this void
保留字 goto const

自增自减运算符

在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。

++和--运算符可以放在操作数之前,也可以放在操作数之后,当运算符放在操作数之前时,先自增/减,再赋值;当运算符放在操作数之后时,先赋值,再自增/减。例如,当“b=++a”时,先自增(自己增加 1),再赋值(赋值给 b);当“b=a++”时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。

continue、break、和return的区别是什么?

在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词:

  1. continue :指跳出当前的这一次循环,继续下一次循环。
  2. break :指跳出整个循环体,继续执行循环下面的语句。

return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:

  1. return; :直接使用 return 结束方法执行,用于没有返回值函数的方法
  2. return value; :return 一个特定值,用于有返回值函数的方法

在循环前命名后,可以使用以上关键字进行多层控制

什么是泛型?什么是类型擦除?介绍一下常用的通配符?

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。泛型擦除

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。

常用的通配符为: T,E,K,V,?通配符介绍

  • ? 表示不确定的 java 类型
  • T(type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值中的Key Value
  • E (element) 代表Element

泛型的使用限制

无法使用基本数据类型

List<int> list = new ArrayList<>(); // 编译时错误

无法创建泛型参数类型的实例

public <E> void test(E e) {
    E e1 = new E(); // 编译时错误
}

但是可以通过反射来创建对象

public <E> void test(E e) {
    E e1 = e.getClass().newInstance();// 正确
}

不能为static字段(属性)声明为泛型类型

因为static字符属于类,多个实例共享。

class Test<T>{
    private static T name; // 编译错误
    private T age; // 正确
}

无法使用泛型类型进行强制类型转换或者instanceof

List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>)li; //编译错误

除非使用无界符号?才可以强制转换

List<?> li = new ArrayList<>();
List<Number> ln = (List<Number>)li;
// 或者
List<Integer> li = new ArrayList<>();
List<?> ln = (List<?>)li;

在某种情况下,编译器知道泛型类型始终有效并允许强制类型转换

List<String> li = new ArrayList<>();
ArrayList<String> ln = (ArrayList<String>)li;

instanceof

public <E> void test(List<E> list) {
    if(list instanceof ArrayList<String>) {} //编译错误
}

运行时是不跟踪参数类型的,所以无法区分泛型类型。可以使用无界符号

public <E> void test(List<E> list) {
    if(list instanceof ArrayList<?>) {} //正确
}

无法创建泛型类型的数组

List<String>[] lists = new ArrayList<String>[2]; //编译时错误
lists[0] = new ArrayList<String>();
lists[1] = new LinkedList<String>(); //java.lang.ArrayStoreException

将不同类型元素插入到数组中:

Object[] objs = new String[2];
objs[0] = "s";
objs[1] = 1; // java.lang.ArrayStoreException

使用集合进行相同的操作

Object[] obs = new List<String>[2]; // 编译错误
obs[0] = new ArrayList<String>();
obs[1] = new ArrayList<Integer>();

无法创建、捕获或者抛出泛型类型异常

泛型不能直接或间接扩展Throwable类。

class Ex<T> extends Exception{} // 编译错误
class Exc<T> extends Throwable{} // 编译错误

无法捕获泛型类型实例

public <T extends Exception> void test() {
    try {
    }catch(T t) { // 编译错误
    }
}

但是可以在throws子句中出现

public <T extends Exception> void test() throws T{}

泛型擦除到原生类型的方法无法重载

​ 因为泛型擦除后,方法的签名一样。

public void test(List<String> list) {} //编译错误
public void test(List<Integer> list) {} // 编译错误

==和equals的区别

== : 它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。(基本数据类型比较的是值,引用数据类型比较的是内存地址)

因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals() : 它的作用也是判断两个对象是否相等,它不能用于比较基本数据类型的变量。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。

Objectequals()方法:

public boolean equals(Object obj) {
    return (this == obj);
}

equals() 方法存在两种使用情况:

  • 情况 1:类没有覆盖 equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。使用的默认是 Objectequals()方法。
  • 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。

说明:

  • String 中的 equals 方法是被重写过的,因为 Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

hashCode()与 equals()

面试官可能会问你:“你重写过 hashcodeequals么,为什么重写 equals 时必须重写 hashCode 方法?”

1) hashCode()介绍:

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。

public native int hashCode();

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

2) 为什么要有 hashCode?

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

3) 为什么重写 equals 时必须重写 hashCode 方法?

如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖

hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

hashCode()与 equals()的相关规定

  1. 如果两个对象相等,则 hashcode ⼀定也是相同的
  2. 两个对象相等,对两个对象分别调⽤ equals ⽅法都返回 true
  3. 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
  4. 因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖
  5. hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode(),则该 class
    的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)

4) 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?

以下内容摘自《Head Fisrt Java》。

因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)

我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。

5) 重写 equals 方法的原则

  • 自反性 :对于任何非空的参考值xx.equals(x)应该返回true
  • 对称性 :对于任何非空引用值xyx.equals(y)应该返回true当且仅当y.equals(x)回报true
  • 传递性 :对于任何非空引用值xyz ,如果x.equals(y)回报truey.equals(z)回报true ,然后x.equals(z)应该返回true
  • 一致性 :对于任何非空引用值xy ,多次调用x.equals(y)始终返回true或始终返回false ,没有设置中使用的信息equals比较上的对象被修改。
  • 对于任何非空的参考值xx.equals(null)应该返回false

基本数据类型

Java中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢?

Java有8种基本数据类型,分别为:

  1. 6种数字类型 :byte、short、int、long、float、double
  2. 1种字符类型:char
  3. 1种布尔型:boolean。

这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean

基本类型 位数 字节 默认值
int 32 4 0
short 16 2 0
long 64 8 0L
byte 8 1 0
char 16 2 'u0000'
float 32 4 0f
double 64 8 0d
boolean 1 1 false

对于boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1位或1字节,但是实际中会考虑计算机高效存储因素。

注意:

  1. Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析:
  2. char a = 'h'char :单引号,String a = "hello" :双引号

8种基本类型的包装类和常量池

Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。

自动拆装箱(看源码就知道了,最终调用的是 valueOf 方法,而这个方法在一定范围内时是走缓存的)。

为什么设置缓存?

  1. 性能方面。see Integer#valueOfas this method is likely to yield significantly better space and time performance by caching frequently requested values

为把啥缓存设置为[-128,127]区间?

  1. 技术规范。JLS7 5.1.7If the value p being boxed is an integer literal of type int between -128 and 127 inclusive (§3.10.1), or the boolean literal true or false (§3.10.3), or a character literal between '\u0000' and '\u007f' inclusive (§3.10.4), then let a and b be the results of any two boxing conversions of p . It is always the case that a == b .
  2. 性能和资源之间的权衡(当然也可以调整缓存的正向最大值,自己看 IntegerCache类的实现)

Integer.java

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

自动装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

更多内容见:深入剖析Java中的装箱和拆箱

数据类型转换

不同的基本数据类型之间进行运算时需要进行类型转换。除布尔类型外,所有的基本数据类型进行运算时都需要考虑类型转换,主要应用在算术运算时和赋值运算时。

存储的位数越多,类型的级别越高。基本数据类型的级别byte级别最低,double级别最高。

算术运算时

除特殊情况外,低级类型和高级别类型进行算术运算时,低级别类型会自动转换为高级别。特殊情况有:

byteshortchar之间运算后都会转换为 int类型。

赋值运算时

赋值运算时,数据类型的转换有自动类型转换和强制类型转换。

1)自动类型转换

将低级别的类型赋值给高级别类型时将进行自动类型转换。

2)强制类型转换

将高级别的类型赋值给低级别的类型时,必须进行强制类型转换。在Java中使用一对小括号进行强制类型转换。

进行强制类型转换时,可能会丢失数据。如:int类型强制转换为byte时,int的低位第一个字节中的8位数据能够转换为byte类型,int类型的高位3个字节中的24位数据会丢失,所以有可能丢失精度。

方法

为什么 Java 中只有值传递?

首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。

Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

example1:

public static void main(String[] args) {
    int num1 = 10;
    int num2 = 20;

    swap(num1, num2);

    System.out.println("num1 = " + num1);
    System.out.println("num2 = " + num2);
}

public static void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;

    System.out.println("a = " + a);
    System.out.println("b = " + b);
}

结果:

a = 20
b = 10
num1 = 10
num2 = 20

解析:

在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。

通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.

example2:

    public static void main(String[] args) {
        int[] arr = { 1, 2, 3, 4, 5 };
        System.out.println(arr[0]);
        change(arr);
        System.out.println(arr[0]);
    }

    public static void change(int[] array) {
        // 将数组的第一个元素变为0
        array[0] = 0;
    }

结果:

1
0

解析:

array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。

通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。

很多程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。

总结

Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。

下面再总结一下 Java 中方法参数的使用情况:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

参考:

《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节

重载和重写的区别

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法

重载

发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。

下面是《Java 核心技术 I》对重载这个概念的介绍:

重写

重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

  1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。

  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static修饰的方法能够被再次声明。

  3. 构造方法无法被重写

    综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变 。

区别点 重载(overload) 重写(override)
发生范围 同一个类 子类 中
参数列表 必须不同 必须相同
返回类型 可以不同 必须相同
异常 可以不同 可以减少或删除,不能抛出新的或者更⼴的异常(运行时异常除外)
访问修饰符 可以不同 可以提高
发生阶段 编译期 运行期

方法的重写要遵循“两同两小一大”(以下内容摘录自《疯狂 Java 讲义》,issue#892 ):

  • “两同”即方法名相同、形参列表相同;
  • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
  • “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

Java面向对象

类和对象

面向对象和面向过程 的区别

  • 面向过程面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
  • 面向对象面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低

这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。

而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。

构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?

主要作用是完成对类对象的初始化工作。

可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,这时候,就不能直接 new 一个对象而不传递参数了,所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。

在 Java 中定义一个不做事且没有参数的构造方法的作用

Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

构造器 Constructor 是否可被 override?

Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。

构造方法有哪些特性?

  1. 名字与类名相同。
  2. 没有返回值,但不能用 void 声明构造函数。
  3. 生成类的对象时自动执行,无需调用。

在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?

创建子类对象前要先创建父类对象,以帮助子类做初始化工作。

成员变量与局部变量的区别有哪些?

  1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  2. 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

创建一个对象用什么运算符?对象实体与对象引用有何不同?

new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系⽓球,也可以系一个⽓球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个⽓球)。

类实例化的过程(即对象的创建过程)

class SuperClass{
    public static int a = 1;
    public int b = 1;
    static{
        //省略代码
        
    }
    {
        //省略代码
    }
    public SuperClass(){
        //省略代码
        show();
    }
    public void show() {
        System.out.println(this.a + "  , " + this.b);
    }
}
class SubClass extends SuperClass{
    public static int a =2;
    public int b = 2;
    static{
        //省略代码
    }
    {
        //省略代码
    }
    public SubClass(){
        //省略代码
        show();
    }
    
    @Override
    public void show() {
        System.out.println(this.a + "  , " + this.b);
    }
}

这里只考虑 SubClassSuperClas,暂不考虑java.lang.Object

​ 这里在new SubClass()对象的时候,首先执行 new。虚拟机发现方法区中并没有该类的信息,就会先去加载该类,在加载的时候发现该类有父类(Java中除了java.lang.Object没有父类,其他类都会有父类)即SuperClass,则会先加载该类的父类(SuperClass),然后进行父类类(static)变量的初始化(包括准备阶段的默认初始化(零值)和初始化阶段的显示初始化(clinit也就是static初始化或者说是static块)。然后进行子类类(static)变量的初始化,类变量初始化完毕。

​ 接着进行对象的创建,进入子类构造函数(SubClass()),在子类构造函数第一行会调用递归父类构造函数(SuperClass),进入父类构造函数之前会先进行父类实例变量初始化,即给非静态成员变量初始化(实例初始化)并运行代码块,然后进入父类构造函数执行,当运行show()方法时发现子类对其进行了覆盖,那么运行子类的show()方法,由于子类还没有进行实例变量的显示初始化,只能输出20 ,父类构造函数运行结束返回到子类构造函数,此时先会进行子类实例变量的显示初始化(实例初始化),运行了子类代码块,接着运行子类构造函数,接着运行show(),这时子类实例变量都进行了显示初始化,输出22

遮蔽(shadowing)

有些声明可能在其作用域的一部分被相同名称的另一个声明遮蔽。

class Test {
    static int x = 1;
    public static void main(String[] args) {
        int x = 0;
        System.out.print("x=" + x);
        System.out.println(", Test.x=" + Test.x);
    }
}

隐藏(hiding)

用于将被继承但由于子类中的声明而不被继承的成员

class Super{
    int x = 10;
    static int y = 20;
}
class Sub extends Super{
    int x = 11;
    static int y = 21;
    
    public void test(){
        System.out.println("this.x = " + this.x); // x in class Sub
        System.out.println("super.x = " + super.x);// x in class Super
        System.out.println("this.y = " + this.y); // y in class Sub
        System.out.println("Sub.y = " + Sub.y);  // y in class Sub
        System.out.println("super.y = " + super.y); // y in class Super
        System.out.println("Super.y =" + Super.y); // y in class Super
    }
    
    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.test();
    }
}

运行结果:

this.x = 11
super.x = 10
this.y = 21
Sub.y = 21
super.y = 20
Super.y =20

static方法可以被继承,但是不会被重写

隐藏和重写(覆盖)

隐藏指的是子类把父类的属性或者方法隐藏了,即将子类强制转换成父类后,调用的还是父类的属性和方法,而覆盖则指的是父类引用指向了子类对象,调用的时候会调用子类的具体方法。

(1) 变量只能被隐藏(包括静态和非静态),不能被覆盖

(2) 可以用子类的静态变量隐藏父类的静态变量,也可以用子类的非静态变量隐藏父类的静态变量,也可以用非最终变量(final)隐藏父类中的最终变量;

(3) 静态方法(static)只能被隐藏,不能被覆盖;

(4) 非静态方法可以被覆盖;

(5) 不能用子类的静态方法隐藏父类中的非静态方法,否则编译会报错;

(6) 不能用子类的非静态方法覆盖父类的静态方法,否则编译会报错;

(7) 不能重写父类中的最终方法(final);

(8) 抽象方法必须在具体类中被覆盖;

Java 面向对象编程三大特征

封装 继承 多态

封装

封装是指把一个对象的状态信息隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。

java中是通过"访问控制符"来实现类中哪些细节可以暴露,哪些细节需要封装隐藏的

继承

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。

关于继承如下 3 点请记住:

  1. 关于子类可以继承父类中可见(可访问)的属性和方法(或者说是除private外的属性和方法)(Java 规范)
    1. 还有一种说法子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。(之前说过)
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。(重写)
  4. 子类不能继承父类的构造方法、初始化模块

多态

多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

多态的特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 对象类型不可变,引用类型可变;
  • 方法具有多态性,属性不具有多态性;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

可以说重载是编译时多态,重写是运行时多态

继承、覆盖和隐藏

  1. 一个类可以从它的直接超类继承该超类的所有具体方法(包括static方法和实例方法)
  2. 一个类可以从它的直接超类和直接父接口中继承`abstractdefault方法
  3. 类不会从它的超接口中继承static方法

Overriding (实例方法)

  1. Overriding发生在继承关系中,重写方法在子类中。子类中的方法签名(方法名和方法参数)
  2. 如果非抽象方法覆盖了超类的抽象方法,称为实现了方法
  3. 如果一个实例方法覆盖了一个static方法,这是一个编译时错误。(字段可以)

Hiding (类方法)

  1. 一个类中声明或继承了一个static方法,那么这个方法就隐藏了其父类或父接口的同签名方法
  2. 如果一个static方法隐藏了一个实例方法,这是一个编译时错误(字段可以)
  3. 可以通过使用限定名或使用super来访问隐藏方法

example:

public class Super {
    
    public static void staticMethod() {
        System.out.println("Super.staticMethod");
    }
    
    public void instanceMethod() {
        System.out.println("super.instanceMethod");
    }
}

class Sub extends Super{
    
    public static void staticMethod() {
        System.out.println("Sub.staticMethod");
    }
    
    @Override
    public void instanceMethod() {
        System.out.println("sub.instanceMethod");
    }
    
}

class Test {
    public static void main(String[] args) {
        Super s = new Sub();
        s.staticMethod();
        s.instanceMethod();
    }
}

console:

Super.staticMethod
sub.instanceMethod

因为调用staticMethod在编译时就要找出要调用方法的s的类型,也就是Super。而调用instanceMethod是在运行时找出要调用实例方法的s的类型,也就是Sub

Super s = new Sub();

换句话说就是,调用static方法看左边的类型也就是说Super。调用实例方法要看右边的类型也就是Sub

修饰符

访问修饰符:

访问修饰符(也称为权限修饰符)有:public / protected / private。还有一种访问权限是不写任何关键字(有的地方称为包访问修饰符)

确认可访问性:

  1. 如果一个类或者接口声明为public,那么它可以被任何代码访问

  2. 未声明访问修饰符的类或接口类型隐式拥有包访问权(Package-Access)

  3. 如果类或接口类型是用包访问声明的,那么只能从声明它的包内访问它。

  4. 引用类型的成员(类、接口、字段或方法)或类类型的构造函数,只有在该类型是可访问的且成员或构造函数声明允许访问时才可访问

    1. 如果成员或构造函数声明为public,则允许访问
    2. 所有缺少访问修饰符的接口成员都是隐式public
  5. 成员或构造函数被声明为private,当且仅当它发生在包含成员或构造函数声明的类的主体中时,访问才被允许

  6. 对象的`protected `成员或构造函数可以从包的外部访问,仅由实现该对象的代码声明。

    package point;
    public class Point {
    protected int x, y;
    void warp(threePoint.Point3d a) {
    if (a.z > 0) // compile-time error: cannot access a.z
    a.delta(this);
    }
    }
    package threePoint;
    import point.Point;
    public class Point3d extends Point{
    protected int z;
    public void delta(Point p) {
    p.x += this.x; // compile-time error: cannot access p.x
    p.y += this.y; // compile-time error: cannot access p.y
    }
    public void delta3d(Point3d q) {
    q.x += this.x;
    q.y += this.y;
    q.z += this.z;
    }
    }
位置 public protected Package-Access private
同类访问
同包其他类访问 ×
同包子类访问 ×
不同包子类访问 × ×
不同包非子类访问 × × ×

final

final关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:

  1. final修饰的类不能被继承,final类中的所有成员方法都会被隐式的指定为final方法;
  2. final修饰的方法不能被重写;
  3. final修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。

说明:使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。

static

static 关键字主要有以下四种使用场景:

  1. 修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名 类名.静态方法名()

    被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。

    方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

    HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。

  2. 静态初始化器(静态代码块): 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.

  3. 静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。

  4. 静态导包(用来导入类中的静态资源,1.5之后的新特性): 格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。

this

this关键字用于引用类的当前实例。

关键字this只能在以下情况下使用:

  • 在实例方法或默认方法的主体中
  • 在类的构造函数的主体中
  • 在类的实例初始化器(代码块 {})中
  • 在类的实例变量的初始化程序中
  • 用于在实例方法中区分局部变量和成员变量

如果它出现在其他任何地方,则会发生编译时错误。

this 仅当在lambda表达式出现的上下文中允许该关键字时,才可以在lambda表达式中使用该关键字。否则,将发生编译时错误。

关键字this还用于显式构造函数调用语句

super

super表示当前类的父类实例,用于从子类访问父类的变量和方法。

使用关键字的super仅在实例方法,实例初始化器或类的构造函数中或在类实例变量的初始化器中有效。如果它们出现在其他任何地方,则会发生编译时错误.

super关键字的使用和this关键字大致相同,只是表示的实例不同。

使用 this 和 super 要注意的问题:

  • 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。所以它们(在调用构造方法时)不能同时出现
  • this、super不能用在static方法中。

被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西

String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?

可变性

简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串, private final char value[] ,所以 String 对象是不可变的。

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串
private final byte[] value

而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在
AbstractStringBuilder 中也是使用字符数组保存字符串 char[] value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是AbstractStringBuilder实现的。(可自行查阅源码)

abstract class AbstractStringBuilder implements Appendable, CharSequence
{
     /**
     * The value is used for character storage.
     */
     char[] value;
     /**
     * The count is the number of characters used.
     */
     int count;
     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% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据(不频繁改变的): 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据(频繁改变的): 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

接口和抽象类的区别是什么?

abstract class和interface有什么区别?

abstract class表示抽象类,interface关键字用于声明接口。

相同:

  • 接口和抽象类都不能被实例化
  • 可以定义一个抽象类或接口类型的引用,用来引用子类或实现类
  • 都可以包含抽象方法

强调: 一个类继承抽象类或实现接口时都可以不实现全部抽象方法,只要将这个类声明为abstract

不同:

  • 一个类只能继承一个抽象类,使用extends关键字;可以实现多个接口,使用implements
  • 抽象类可以有实例成员,static成员、抽象方法,抽象类中的方法不能有default修饰;接口只能有常量、抽象方法,接口的方法默认是 public修饰的,JDK8及之后版本可以有static方法和default方法,JDK9以后可以有private方法
  • 抽象类中的抽象方法可以使用publicprotected以及package访问修饰符修饰,子类实现抽象方法时不予许缩小访问权限;接口中的抽象方法只能是public,实现类实现抽象方法只能是public
  • 抽象类有构造方法,接口没有
  • 接口强调特定功能的实现,而抽象类强调所属关系(从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。)
  • 接口自己本身可以通过 extends 关键字扩展多个接口。

备注:

  1. 在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。
  2. JDK9 的接口被允许定义私有方法

总结一下JDK7 ~JDK9 Java 中接口概念的变化相关内容

  1. 在JDK 7 或更早版本中,接口⾥面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。
  2. JDK8 的时候接口可以有默认方法和静态方法功能。
  3. JDK 9 在接口中引入了私有方法和私有静态方法。

成员变量与局部变量的区别有哪些?

  1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

  2. 从变量在内存中的存储方式来看:如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引用数据类型,那存放的是指向堆内存对象的引用或者是指向常量池中的地址。

  3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。

  4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

深拷贝 vs 浅拷贝

  1. 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
  2. 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

Object类

Object 类是一个特殊的类,是所有类的父类。

所有类和数组类型都能自Object类继承方法,总结如下: (Java 规范中这样描述)

  • clone方法用于复制对象

  • `equals `定义了对象相等的概念,这是基于值而不是引用的比较。

  • `finalize `在销毁对象之前运行

  • getClass返回表示该对象的Class对象

    每个引用类型都有一个Class对象。例如,可以使用它来获取类的完全限定名、它的成员、它的直接超类和它实现的任何接口。

  • `hashCode `这个方法非常有用,在诸如java.util.HashMap这样的哈希表中,方法hashCode与方法equals一起非常有用

  • 方法waitnotifynotifyAll在使用线程的并发编程中使用

  • `toString `返回对象的字符串表示形式

它主要提供了以下 11 个方法:


public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

Java 中提供了一个 Object 的工具类 java.util.Objects(自行了解)

集合

Java中的集合存放于java.util包下,主要从三个方面入手:

  1. Collection:Collection 是集合 List、Set、Queue 的最基本的接口。

  2. Iterator:迭代器,可以通过迭代器遍历集合中的数据

  3. Map:是映射表的基础接口

说说List,Set,Map三者的区别?

  • List: List接口存储一组不唯一,有序的对象,可以存 NULL
  • Set: 不允许重复的集合。
  • Map: 使用键值对存储,一组键值组合称为Entry。Map会维护与Key有关联的值。两个Key可以存相
    同的对象,但Key不能重复。

Arraylist 与 LinkedList 区别?

相同点:

都是java.util.List接口的实现类;都是有序、可排序、可重复的集合;都支持迭代器操作

不同:

  1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是非线程安全;
  2. 底层数据结构: Arraylist 底层使用的是数组; LinkedList 底层使用的是双向链表 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  3. 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第i和第i个元素之后的(n-i)个元素都要执行向后位/向前移⼀位的操作。 ② LinkedList 采用链表存储,所以对于 add(E e)方法法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话
    ( (add(int index, E element) ) 时间复杂度近似为 o(n) 因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) 方法)。
  5. 内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留⼀定的容量空间,而LinkedList的空间花费则体现在它的每⼀个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
  6. 实现接口不同:ArrayList实现了 RandomAccess 接口,LinkedList 实现了Queue 接口 和 Deque 接口,支持队列操作,同时支持栈操作
  7. 扩容方式:ArrayList 内部采用 倍数增长 的方式扩容,LinkedList内部采用链表实现,不需要扩容

补充内容:RandomAccess接口

public interface RandomAccess {}

查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,RandomAccess 接口不过是⼀个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。

java.util.Collections.binarySearch()⽅法中,它要判断传⼊的list 是否 RamdomAccess 的实例,如果是,调⽤
indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

ArrayList 实现了 RandomAccess 接⼝, ⽽ LinkedList 没有实现。为什么呢?我觉得还是
和底层数据结构有关! ArrayList 底层是数组,⽽ LinkedList 底层是链表。数组天然⽀持随机
访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的
元素,时间复杂度为 O(n),所以不⽀持快速随机访问。 ArrayList 实现了 RandomAccess 接⼝,就表明了他具有快速随机访问功能。 RandomAccess 接⼝只是标识,并不是说 ArrayList 实现 RandomAccess 接⼝才具有快速随机访问功能的!

下⾯再总结⼀下 list 的遍历⽅式选择:

  • 实现了 RandomAccess 接⼝的list,优先选择普通 for 循环 ,其次 foreach
  • 未实现 RandomAccess 接⼝的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的)

补充内容:双向链表和双向循环链表

双向链表: 包含两个指针,⼀个prev指向前⼀个节点,⼀个next指向后⼀个节点。

双向循环链表: 最后⼀个节点的 next 指向head,⽽ head 的prev指向最后⼀个节点,构成⼀个环。

另外推荐⼀篇把双向链表讲清楚的文章:https://juejin.im/post/5b5d1a9af265da0f47352f14

ArrayList 与 Vector 区别呢?

相同点: 都是List的实现类,底层都是通过数组实现的

不同点:

扩容方式不同(具体请查看源码)

线程安全:Vector 类的所有⽅法都是同步的,即线程安全。但是⼀个线程访问Vector的话代码要在同步操作上耗费⼤量的时间。
Arraylist 不是同步的,即非线程安全,所以在不需要保证线程安全时建议使⽤Arraylist

ArrayList 的扩容机制

HashMap 和 Hashtable 的区别

相同:

两者都实现 Map 接口,用于存放 键-值对

内部都采用 哈希表 实现,都采用 哈希算法计算键-值对的存放位置

不同:

  1. 线程是否安全: HashMap 是⾮线程安全的,HashTable 是线程安全的;HashTable 内部的⽅法
    基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap)

  2. 效率: 因为线程安全的问题,HashMap 要⽐ HashTable 效率⾼⼀点。

  3. Null keyNull value的⽀持: HashMap 中,null 可以作为键,这样的键只有⼀个,可以
    有⼀个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有⼀个 null
    直接抛出 NullPointerException,即不支持 nullkeyvalue

  4. 初始容量⼤⼩和每次扩充容量⼤⼩的不同 : ①创建时如果不指定容量初始值,Hashtable 默认
    的初始⼤⼩为11,之后每次扩充,容量变为原来的2n+1HashMap 默认的初始化⼤⼩为16。之后
    每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤
    你给定的⼤⼩,⽽ HashMap 会将其扩充为2的幂次⽅⼤⼩(HashMap 中的 tableSizeFor()
    法保证)。也就是说 HashMap 总是使⽤2的幂作为哈希表的⼤⼩,后⾯会介绍到为什么是2的幂次⽅。

  5. 内部实现:

    JDK 1.8 ( Java 8 )开始,HashMap内部采用 数组+链表+红黑树方式存储

​ 当链表长度大于8时会自动转换成红黑树(内部还会判断存储的元素Node数量是否大于 64)

​ 当链表长度小于6时,红黑树重新转换成链表

​ 而 Hashtable 内部则采用 数组 + 链表 实现

  1. 元素位置的计算方法:

    HashMap 内部采用一个单独的方法根据 key.hashCode() 重新计算一个哈希值后再确定元素存放位置

    Hashtable 内部直接采用 key.hashCode() 来确定元素存放位置

  2. 继承类:

    HashMap 类继承 AbstractMap

Hashtable 类继承 Dictionary

附:HashMapput方式(图上有小误差,以我上课时讲的为准)

HashMap 和 HashSet区别

HashSet 源码就知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码⾮常⾮常少,因为除了 clone()writeObject()readObject()HashSet ⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。

HashMap HashSet
实现了Map接⼝ 实现Set接⼝
存储键值对 仅存储对象
调⽤ put()
map中添加元素
调⽤ add() ⽅法向Set中添加元素
HashMap使⽤键
(Key)计算
hashcode
HashSet使⽤成员对象来计算hashcode值,对于两个对象来说hashcode可能
相同,所以equals()⽅法⽤来判断对象的相等性,

HashSet如何检查重复

当你把对象加⼊ HashSet 时,HashSet会先计算对象的 hashcode 值来判断对象加⼊的位置,同时也会
与其他加⼊的对象的hashcode值作比较,如果没有相符的hashcodeHashSet会假设对象没有重复出
现。但是如果发现有相同hashcode值的对象,这时会调⽤ equals() ⽅法来检查hashcode相等的对
象是否真的相同。如果两者相同,HashSet就不会让加⼊操作成功。(摘⾃《Head fist java》第⼆版)

hashCode()equals()的相关规定:hashCode()与 equals()

标签


© 2021 成都云创动力科技有限公司 蜀ICP备20006351号-1