Java

Java基础

Java语言的特点

  • 面向对象(封装,继承,多态)
  • 平台无关性( Java 虚拟机实现平台无关性)
  • 支持多线程
  • 支持网络编程并且很方便
  • 编译与解释并存

    关于 JDK & JVM & JRE

    JVM

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

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

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

image.png

  1. 我们需要格外注意的是 .class-> 机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。

JDK & JRE

  • JDK (Java Development Kit) 是功能齐全的Java SDK (软件开发工具包),它拥有 JRE 所拥有的一切,还有编译器 (javac),和工具 (如 javadoc [API文档] 和 jdb [Java调试器] )
  • JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机,Java 类库,Java 命令和一些其他的基础构件。但是,它不能用于创建新程序。
  • 总结:
    • JDK = JRE + 开发工具集
    • JRE = JVM + Java SE标准类库

OracleJDK & OpenJDK 的对比

Java & C++ 的区别

  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承(虽然 Java 的类不可以多继承,但是接口可以多继承)
  • Java 有自动内存管理机制,不需要程序员手动释放无用内存
  • 在 C 语言中,字符串或字符数组最后都会有一个额外的字符 ‘\0’ 来表示结束。但是,Java 语言中没有结束符这一概念

    Java 语言 “编译与解释并存” 的说明

  • 高级编程语言按照程序的执行方式分为编译型语言和解释型语言两种

  • 编译型语言是指:编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码
  • 解释型语言是指:解释器对源程序逐行解释成特定平台的机器码并立即执行
  • Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(*.class 文件),这种字节码必须由 Java 解释器来解释执行

    字符常量 & 字符串常量的区别

  • 形式上: 字符常量是单引号引起的⼀个字符,字符串常量是双引号引起的若干个字符

  • 含义上: 字符常量相当于⼀个整型值( ASCII 值),可以参与表达式运算。字符串常量代表⼀个地址值 (该字符串在内存中存放位置)
  • 占内存大小:字符常量只占 2 个字节,字符串常量占若干个字节
    • java 编程思想第四版: 2.2.2 节
      • Java 要确定每种基本类型所占存储空间的大小。它们的大小并不像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一

Java 泛型 & 类型擦除 & 通配符 的说明

  • Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数
  • Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除
  • 泛型一般有三种使用方式:泛型类、泛型接口、泛型方法

    1. //泛型类
    2. public class Generic<T>
    3. //实例化泛型类
    4. Generic<Integer> genericInteger = new Generic<Integer>();
    5. //泛型接口
    6. public interface Generator<T>
    7. //实现泛型接口,指定类型
    8. class GeneratorImpl<T> implements Generator<String>
    9. //泛型方法
    10. public static < E > void printArray( E[] inputArray )
  • 常用的通配符

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

      == & equals 的区别

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

  • equals():它不能用于比较基本数据类型的变量。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类

    • String 中的 equals()是被重写过的,比较的是对象的值
    • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象
      1. //Object类中的equals()
      2. public boolean equals(Object obj) {
      3. return (this == obj);
      4. }

      hashCode() & equals()

  • hashCode() 作用是获取哈希码,也称为散列码,它返回一个 int 整数。这个哈希码的作用是:确定该对象在哈希表中的索引位置。定义在Object类中的 hashCode()是本地方法,也就是用 C 语言 或 C++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回

  • 以“HashSet 如何检查重复”为例来说明为什么要有 hashCode()
    • 当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入。如果不同的话,就会重新散列到其他置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度
  • 重写 equals() 时必须重写 hashCode()的原因
    • 如果不这样做的话,就会违反 Object.hashCode 的通用的约定,从而导致该类无法结合所有基于散列的集合正常运作,这样的集合包括HashMap、HashSet、HashTable等
    • equals() 和 hashCode()间的关系是这样的
      • 如果两个对象相同(即:用 equals 比较返回true),那么它们的 hashCode 值要相同
      • 如果两个对象的 hashCode 值相同,它们并不一定相同(即:用 equals 比较返回 false)
    • 原因是保证:同一个对象,在 equals() 比较相同的情况下 hashcode 值必定相同

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

基本数据类型 & 包装类型 & 常量池

  • Java 中有 8 种基本数据类型

    • 6 种数字类型、1 种字符类型、1 种布尔型 | 基本类型 | 位数 | 字节 | 基本类型的默认值 | 包装类型 | | —- | —- | —- | —- | —- | | boolean | 1 | | false | Boolean | | byte | 8 | 1 | 0 | Byte | | char | 16 | 2 | ‘u0000’ | Character | | short | 16 | 2 | 0 | Short | | int | 32 | 4 | 0 | Integer | | float | 32 | 4 | 0f | Float | | double | 64 | 8 | 0d | Double | | long | 64 | 8 | 0L | Long |
  • Java 基本类型的包装类的大部分都实现了常量池技术(包装类 Float,Double 并没有实现常量池技术)。Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False

    所有整型包装类对象之间值的比较,全部使用 equals 方法比较

装箱 & 拆箱

  • 装箱是调用了 包装类的valueOf()方法
  • 拆箱是调用了 xxxValue()方法
  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)
  • int n = i 等价于 int n = i.intValue();

    在一个静态方法内调用一个非静态成员是非法的

  • 静态方法属于类,在类加载的时候就会分配内存,可以通过类名直接访问

  • 而非静态成员属于实例对象,只有在对象实例化之后才存在,然后通过类的实例对象去访问
  • 在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作(需要结合 JVM 的相关知识)

    静态方法 & 实例方法 的不同

  • 在外部调用静态方法时,可以使用 “类名.方法名” 和 “对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象

  • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制

    Java 只有值传递

  • 按值调用(call by value) 指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数

  • 按引用调用(call by reference) 是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数
  • Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝

    重载和重写的区别

  • 如果多个方法有相同的名字、不同的参数,便产生了重载

    • 构造方法可以被重载,但无法被重写
  • 重写是子类中的方法对父类中的某一同名、同参、同返回类型的方法的重新编写
    • 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明
    • 如果方法的返回类型是void和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时可以返回该引用类型的子类 | 区别点 | 重载方法 | 重写方法 | | —- | —- | —- | | 发生范围 | 同一个类 | 子类 | | 参数列表 | 必须修改 | 一定不能修改 | | 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等(针对引用类型) | | 异常 | 可修改 | 子类方法声明抛出的异常类范围必须小于等于父类(异常要更精确) | | 访问修饰符 | 可修改 | 访问修饰符范围大于等于父类(public>pritected>default>privata) | | 发生阶段 | 编译期 | 运行期 |

深拷贝 & 浅拷贝

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

image.png

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

  • 面向过程是:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用
  • 面向对象是:把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为
  • 面向对象 :面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。面向对象包含了面向过程的思想。
  • Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。而面向过程语言大多都是直接编译成机械码在电脑上执行。

    关于 封装 & 继承 & 多态

  • 封装:把一个对象的属性私有化,同时提供一些外界访问属性的方法,如果属性不想被外界访问,我们不提供方法给外界即可

  • 继承:继承是使用已存在类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用父类的代码
    • 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是子类无法访问父类中的私有属性和方法
  • 多态:表示一个对象具有多种状态。具体表现为父类的引用指向子类的实例

    • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定
    • 多态不能调用 “只在子类存在但不在父类存在” 的方法
    • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法

      String & StringBuffer & StringBuilder

  • 可变性

    • String 类中使用 final 关键字修饰字符数组来保存字符串,所以String 对象是不可变的
    • StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,没有用 final 关键字修饰,所以这两种对象都是可变的

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

  • 线程安全性

    • String 中的对象是不可变的,也就可以理解为常量,是线程安全的
    • StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的
    • StringBuilder 没有对方法加同步锁,所以是非线程安全的 | String | 不可变 | 线程安全 | | —- | —- | —- | | StringBuffer | 可变 | 线程安全 | | StringBuilder | 可变 | 线程不安全 |

Object 类的常见方法总结

关于 反射机制

  • 反射是动态获取信息以及动态调用对象方法的一种机制
  • 通过反射可以获取、调用任意一个类的所有属性和方法
  • 反射 的优缺点
    • 优点 :可以让代码更加灵活、为各种框架提供开箱即用的功能提供了便利
    • 缺点 :它赋予了我们在运行时分析类以及执行类中方法的能力,这同样也增加了安全问题
      • 比如
      • 可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)
      • 反射的性能也稍差点,但对于框架来说实际影响不大
  • 反射 的应用场景

Java集合

https://www.javatpoint.com/collections-in-java
面试八股文 - 图5
image.png

  • 从上图可以看出,在 Java 中除了以 Map 结尾的类之外, 其他类都实现了 Collection 接口。并且,以 Map 结尾的类都实现了 Map 接口