Java基础

Java基础

一、基本概念

1、Java语言特点

  • 平台无关性(Java虚拟机实现平台无关性)。
  • 支持多线程(C++语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持)。
  • 编译与解释并存。一次编写,随处运行。
  • 面向对象(封装、继承、多态)。

2、Java SE 与 Java EE

  • Java SE(Java Platform,Standard Edition):Java 平台标准版,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建桌面应用程序或简单的服务器应用程序。
  • Java EE(Java Platform,Enterprise Edition ):Java 平台企业版,建立在 Java SE 的基础上,包含了支持企业级应用程序开发和部署的标准和规范(比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序。

简而言之,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。

3、JVM、JRE、JDK

JVM虚拟机:运行java字节码的虚拟机。JVM 有针对不同系统(Windows,Linux,macOS)的特定实现,目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。JVM 并不是只有一种。JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。

JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。

JDK(Java Development Kit),它是功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 Java 程序的开发套件。它包含了 JRE,Java基础类库,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具(Java开发工具)。

运行在 Java 虚拟机之上的编程语言

JDK 包含 JRE

4、字节码

字节码:JVM可以理解的代码(即扩展名为 .class的文件)

好处:相对高效(没有C++,Rust,Go语言高效),并且字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

5、JAVA程序从源代码到运行

Java 程序从源代码到运行的过程如下图所示:

Java程序转变为机器代码的过程

Java程序转变为机器代码的过程

JVM首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较。因此引进JIT
JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。这也解释了我们为什么经常说Java 是编译与解释共存的语言

JDK、JRE、JVM、JIT 关系:

JDK、JRE、JVM、JIT 这四者的关系

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

编译型:编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。

解释型:解释型语言会通过解释器一句一句的将代码解释为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。

编译型语言和解释型语言

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

二、基本语法

1、注释的几种形式

单行注释、多行注释、文档注释。

2、标识符和关键字

标识符就是一个普通名字。

关键字是被赋予特殊含义的标识符。

3、Java关键字汇总

img

常见关键字:

  • this:表示当前对象的引用。它可以在类的方法内部使用,通过this关键字,可以访问当前对象的成员变量和方法。
  • super:表示父类的引用。可以在子类中使用,用于引用父类的成员变量和方法。
  • static:表示静态的,属于类本身而不是实例化对象;它可以修饰变量、方法、代码块和内部类;静态变量由所有实例共享,静态方法可以直接通过类名调用;静态代码块在类加载时执行,用于初始化静态成员。
  • final:表示最终的,用于限制变量、方法和类的特性;用于修饰变量时,表示该变量的值不可修改;用于修饰方法时,表示该方法不可被子类重写,即该方法是最终实现;用于修饰类时,表示该类不可被继承,即该类不允许有子类。

4、自增自减运算符

当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。

5、continue,break 和 return

  • continue:跳出当前循环,继续下一次循环 。
  • break:跳出整个循环体,继续执行循环下面的语句。
  • return: 用于跳出所在方法,结束该方法的运行。一般有两种用法,return: 直接使用 return 结束方法执行,用于没有返回值函数的方法。 return value:return 一个特定值,用于有返回值函数的方法。

6、变量

6.1、成员变量与局部变量

语法形式:**成员变量是属于类,而局部变量属于方法或者代码块;**成员变量可以public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的(静态变量),那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的(实例变量),而对象存在于堆内存,局部变量则存在于栈内存。

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

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

6.2、静态变量的作用

静态变量属于类本身,可以被类的所有实例共享,无论一个类创建多少个对象,他们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。

6.3、字符型常量和字符串常量
  • 形式:单引号 和 双引号的区别
  • 含义: 字符型常量相当于整型值(ASCII值),参与表达式运算;字符串常量相当于地址值
  • 占内存大小: 字符常量占2个字节;字符串常量占若干个字节

7、方法

7.1、静态方法和实例方法
  • 调用方式
    静态方法:类名.方法名 或者 对象.方法名 (不建议)
    实例方法: 需要创建对象,对象.方法名
  • 访问类成员是否有限制
    静态方法只允许访问静态成员,不允许访问实例成员
7.2、静态方法为什么不能调用非静态成员?

这个需要结合 JVM 的相关知识,主要原因如下:

  1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
  2. 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

简而言之,就是非静态成员只有在实例化对象后才会存在,而静态方法在类加载时就已经分配内存存在了,如果直接调用不存在的非静态成员,属于非法操作。

7.3、重载和重写
  • 重载:发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。总之,重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理

  • 重写:重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。1、方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。2、如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。3、构造方法无法被重写。总之重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。

img

7.4、可变长参数

在最后一个形参后面加上三个点,表示该形参可以接受多个参数值,允许在调用方法时传入不定长度的参数。可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。

public static void method1(String… args) {
//…
}

public static void method2(String arg1, String… args) {
//…
}

遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?

答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。

三、基本数据类型

1、8种基本数据类型

img

2、基本数据类型和包装类型

  • 默认值: 基本类型有默认值且不是null,包装类型没有默认值,不赋值就是null
  • 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。
  • 存储位置:包装类型属于对象,存放在堆内存基本类型:局部变量放在栈,成员变量放在堆内存
  • 占用空间:基本数据类型占用空间小
  • 比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。

3、自动装箱和拆箱

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

Integer i = 10; //装箱
int n = i; //拆箱

调用的方法:装箱调用了valueof方法,拆箱用了xxxValue()方法

Integer i = 10 等价于 Integer i = Integer.valueOf(10)
int n = i 等价于 int n = i.intValue();

4、浮点数运算的精度丢失

4.1、精度丢失代码演示:

float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false

4.2、为什么会出现精度丢失?

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

4.3、如何解决浮点数精度丢失问题?

BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

BigDecimal a = new BigDecimal(“1.0”);
BigDecimal b = new BigDecimal(“0.9”);
BigDecimal c = new BigDecimal(“0.8”);

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract©;

System.out.println(x); /* 0.1 /
System.out.println(y); /
0.1 /
System.out.println(Objects.equals(x, y)); /
true */

5、超过long整型的数据如何表示?

在 Java 中,64 位 long 整型是最大的整数类型。如果超过这个范围就会有数值溢出的风险。

long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。或者可以用BigDecimal

相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。

6、short s1 = 1;s1 = s1 + 1;有什么错?那么 short s1 = 1; s1 += 1呢?有没有错误?

对于 short s1 = 1; s1 = s1 + 1; 来说,在 s1 + 1 运算时会自动提升表达式的类型为 int ,那么将 int 型值赋值给 short 型变量,s1 会出现类型转换错误。
对于 short s1 = 1; s1 += 1; 来说,+= 是 Java 语言规定的运算符,Java 编译器会对它进行特殊处理,因此可以正确编译。复合赋值运算符+=里面隐含强制类型转换。

7、 Integer变量和Int变量比较

Integer 变量和 int 变量比较时,只要两个变量的值是相等的,则结果为 true。因为包装类 Integer 和基本数据类型 int 类型进行比较时,Java 会自动拆包装类为 int,然后进行比较,实际上就是两个 int 型变量在进行比较。

四、面相对象

1、面向对象和面向过程

面向过程:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
面向对象:会先抽象出对象,然后用对象执行方法的方式解决问题。

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

new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。

  • 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
  • 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。

3、对象相等和引用相等。

  • 对象相等:内存中存放的内容是否相等。
  • 引用相等:指向的内存地址是否相等。

4、类的构造方法

4.1、定义

构造方法是一个与类同名且没有返回值类型的方法,用于创建和初始化对象。

4.2、如果一个类没有声明构造方法,该程序能正确执行吗?

如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。

4.3、构造方法有哪些特点?是否可被 override?

构造方法特点如下:

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

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

5、面向对象三大特征

封装

把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。如USB接口。

继承

子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。

子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

子类可以用自己的方式实现父类的方法。

多态

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

多态的特点:

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

因为继承的特性,我们可以使用一个父类Animal定义各种属性,从而在子类中只需定义独有的方法(比如eat())即可,但是当我们使用这些方法时,他是如何得知我们调用的是哪个类中的eat方法呢?
同样是eat方法,当传入Dog对象时,eat表现出来的行为是吃狗粮,当传入Cat对象时, eat表现出来的就是吃猫粮。
多态性就是同样的一件事情(行为/方法),发生在不同的对象上,表现出不同的结果称之为多态性。多态的实现要求必须是共有继承。提高了代码的灵活性,提高了代码的耦合性(配合反射)。

6、接口和抽象

相同点:

  • 都不能被实例化。
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

区别:

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 一个类只能继承一个类,但是可以实现多个接口。
  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

补充:

抽象方法,是指没有方法体的方法,同时抽象方法还必须使用关键字abstract做修饰
而拥有抽象方法的类就是抽象类,抽象类要使用abstract关键字声明。

  • 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public;
  • 抽象类不能直接实例化,需要依靠子类采用向上转型的方式处理;
  • 抽象类必须有子类,使用extends继承,一个子类只能继承一个抽象类;
  • 子类(如果不是抽象类)则必须覆写抽象类之中的全部抽象方法(如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。);

7、浅拷贝、深拷贝、引用拷贝(克隆)

  • 引用拷贝:引用拷贝就是两个不同的引用指向同一个对象

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象

  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象

浅拷贝、深拷贝、引用拷贝示意图

8、Java中创建对象的常见方法

  • 使用 new 关键字。
  • 使用 Class 类的 newInstance 方法,该方法调用无参的构造器创建对象(反射):Class.forName.newInstance()。
  • 使用 clone() 方法。
  • 反序列化,比如调用 ObjectInputStream 类的 readObject() 方法。

9、如何实现对象的克隆

  • 实现 Cloneable 接口并重写 Object 类中的 clone() 方法。
  • 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。

10、对象的复制

  • 将 A 对象的值分别通过 set 方法加入 B 对象中。
  • 重写 java.lang.Object 类中的方法 clone()。
  • 工具类BeanUtils和PropertyUtils进行对象复制。
  • 通过序列化实现对象的复制。

五、Java的常见类

1、Object类

1.1、定义

Object 类是一个特殊的类,是所有类的父类,11种方法如下:

/**

  • native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
    /
    public final native Class<?> getClass()
    /
    *
  • native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
    /
    public native int hashCode()
    /
    *
  • 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
    /
    public boolean equals(Object obj)
    /
    *
  • native 方法,用于创建并返回当前对象的一份拷贝。
    /
    protected native Object clone() throws CloneNotSupportedException
    /
    *
  • 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
    /
    public String toString()
    /
    *
  • native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
    /
    public final native void notify()
    /
    *
  • native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
    /
    public final native void notifyAll()
    /
    *
  • native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
    /
    public final native void wait(long timeout) throws InterruptedException
    /
    *
  • 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
    /
    public final void wait(long timeout, int nanos) throws InterruptedException
    /
    *
  • 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
    /
    public final void wait() throws InterruptedException
    /
    *
  • 实例被垃圾回收器回收的时候触发的操作
    */
    protected void finalize() throws Throwable { }
1.2、==和equals()的区别
  • ==的使用
    基本数据类型:比较值
    引用数据类型 :比较对象的内存地址
  • equlas的使用
    作用:不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals方法存在于object类中,所有的类都有equals()方法
    使用的两种情况: 类没有重写equals方法,等价于 == ;重写了equals方法
1.3、HashCode

hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。

hashCode() 方法

  • 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
  • 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
  • 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
1.4、为什么重写equals方法需要重写hashcode?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

2、String类

2.1、String、StringBuilder、StringBuffer的区别
  • **可变性:**string不可变。
  • **线程安全性:**string线程安全;StringBuffer加了锁,线程安全;StringBuilder非线程安全。
  • **性能:**StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
  • **使用总结:**操作少量数据,用string;单线程操作字符串缓冲区下操作大量数据,用StringBuilder;多线程操作字符串缓冲区下操作大量数据,用StringBuffer。
2.2、解释String的不可变

String 类中使用 final 关键字修饰字符数组来保存字符串。

  • 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  • String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

不可变性的好处:

  • **可以缓存hash值:**因为string的hash值经常被使用,例如用String用作HashMap的Key。不可变的特性也让hash值也不变。
  • **常量池优化:**String对象创建后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
  • **线程安全:**不可变性保证了不可变性。
2.3、Java9为何把String的底层实现由char[]改成byte[]

Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。

2.4、字符拼接:+ 和 stringbuilder

Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。

字符串对象通过**“+”的字符串拼接方式**,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。

JDK 9 之后,你可以放心使用“+” 进行字符串拼接。了

2.5、String#equals() 和 Object#equals()
  • String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。
  • Object 的 equals 方法是比较的对象的内存地址。
2.6、字符串常量池
2.6.1、作用

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = “ab”;
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = “ab”;
System.out.println(aa==bb);// true aa和bb的地址相同,避免重复创建

2.6.2、String s1 = new String(“abc”);这句话创建了几个字符串对象?
  • 如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。
  • 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
2.6.3、intern 方法有什么作用?

String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

  • 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
  • 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。

// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = “Java”;
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String(“Java”);
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true

2.7、String 类型的变量和常量做“+”运算时发生了什么?

String str1 = “str”;//
String str2 = “ing”;
String str3 = “str” + “ing”;//常量池
String str4 = str1 + str2;//堆
String str5 = “string”;//常量池
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
//加了final关键字
final String str1 = “str”;//加了final关键字就是常量
final String str2 = “ing”;
// 下面两个表达式其实是等价的
String c = “str” + “ing”;// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true

补充常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。

对于 String str3 = “str” + “ing”; 编译器会给你优化成 String str3 = “string”; 。

对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

String str4 = new StringBuilder().append(str1).append(str2).toString();

尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。

六、异常

1、异常层次结构图

Java 异常类层次结构图

2、 Exception和Error的区别

exception和error共同祖先 java.lang中的throwable

exception: 程序本身可以处理的异常,可以用catch来捕获。具体又可以分为checked exception必须处理unchecked exception可以不处理

  • Checked Exception(受检异常):这种异常在编译时就可以被检测出来,必须要在代码中进行处理或者声明抛出,否则编译不通过。这类异常主要是由程序的外部环境引起的,例如文件不存在、网络连接失败等。常见的Checked Exception包括IOException、SQLException等。
  • Unchecked Exception(非受检异常):这种异常通常是由程序内部错误引起的,例如NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException等。这类异常不需要在代码中声明抛出,也可以不进行处理,但是如果不进行处理,程序会崩溃

**error:**程序无法处理的异常,不建议用catch来捕获。
img

3、Throwable常用方法

  • String getMessage():返回异常发生时的简要描述
  • String toString():返回异常发生时的详细信息
  • String getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息

4、try-catch-finally如何使用

  • try: **用于捕获异常。**其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch:用于处理 try 捕获到的异常。
  • finally: **无论是否捕获或处理异常,finally 块里的语句都会被执行。**当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。**注意不要在 finally 语句块中使用 return!**当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。(看JavaGuide详解)

try {
System.out.println(“Try to do something”);
throw new RuntimeException(“RuntimeException”);
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
} finally {
System.out.println(“Finally”);
}

Try to do something
Catch Exception -> RuntimeException
Finally

5、finally中的语句一定执行吗?

不一定。以下几种情况:

  • finally之前虚拟机被终止运行。
  • 程序所在线程死亡。
  • 关闭CPU。

6、如何使用 try-with-resources 代替try-catch-finally?

  • 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象。
  • 关闭资源和 finally 块的执行顺序:try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行。

7、异常使用注意的地方

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。
  • 抛出的异常信息一定要有意义。
  • 建议抛出更加具体的异常。
  • 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。

8、try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

**会。程序在执行到 return 时会首先将返回值存储在一个指定的位置,其次去执行 finally 块,最后再返回。**因此,对基本数据类型,在 finally 块中改变 return 的值没有任何影响,直接覆盖掉;而对引用类型是有影响的,返回的是在 finally 对 前面 return 语句返回对象的修改值。

9、try-catch-finally 中那个部分可以省略?

**catch 和 finally可以省略其中一个,但必须保留其中一个。**try 只适合处理运行时异常,try+catch 适合处理运行时异常+普通异常。也就是说,如果你只用 try 去处理普通异常却不加以 catch 处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用 catch 显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以 catch 可以省略,你加上 catch 编译器也觉得无可厚非。

10、运行时异常(非受检异常)与受检异常有何异同?

  • **运行时异常(非受检异常):**空指针异常、指定的类找不到、数组越界、方法传递参数错误、数据类型转换错误。可以编译通过,但是一运行就停止了,程序不会自己处理。
  • **受检异常:**要么用 try … catch… 捕获,要么用 throws 声明抛出,交给父类处理。

11、throw 和 throws 的区别?

  • throw:在方法体内部,表示抛出异常,由方法体内部的语句处理;throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例。
  • throws:在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理;表示出现异常的可能性,并不一定会发生这种异常。

七、泛型

1、定义及作用

Java 泛型(Generics)是JDK 5中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>() 这行代码就指明了该 ArrayList 对象只能传入 Person 对象,如果传入其他类型的对象就会报错。

2、泛型的使用方法

泛型类、泛型接口、泛型方法。

3、项目中哪里使用了泛型?

  • 自定义接口通用返回结果 CommonResult 通过参数 T 可根据具体的返回类型动态指定结果的数据类型
  • 定义 Excel 处理类 ExcelUtil 用于动态指定 Excel 导出的数据类型
  • 构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)

八、反射

1、定义及优缺点

img

通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。

优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。

缺点:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

2、应用场景

框架、注解。

应用举例:工厂模式,使用反射机制,根据全限定类名获得某个类的 Class 实例。

3、获取对象的四种方式
  • 知道具体类的情况下可以使用。
  • 通过 Class.forName()传入类的全路径获取。
  • 通过对象实例instance.getClass()获取。
  • 通过类加载器xxxClassLoader.loadClass()传入类路径获取。

九、注解

1、定义

Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。注解本质是一个继承了Annotation 的特殊接口。

2、注解的解析方式有哪几种?

注解只有被解析之后才会生效,常见的解析方法有两种:

  • 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override
    注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
  • 运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value 、@Component)都是通过反射来进行处理的。

十、SPI

1、作用及优缺点

  • SPI 即 Service Provider Interface,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
  • 优点:能够大大地提高接口设计的灵活性
  • 缺点:需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的;当多个ServiceLoader同时load时,会有并发问题。
  • SPI接口的使用,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

2、SPI和API的区别

img

API是实现方提供接口,SPI是调用方提供接口。

SPI提供实现的实现类打包成Jar文件,这个Jar文件里面必须有META-INF目录,其下又有services目录,其下有一个文本文件,文件名即为SPI接口的全名,文件的内容该jar包中提供的SPI接口的实现类名。

img

十一、序列化

1、定义及作用

  • 序列化: 将数据结构或对象转换成二进制字节流的过程。
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。
  • 序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

2、序列化的应用场景

对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化。

将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化。

将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

3、transient关键字

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。

transient 只能修饰变量,不能修饰类和方法。

transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。

static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

4、JDK自带序列化定义及缺点

  • JDK 自带的序列化,只需实现 java.io.Serializable接口即可。
  • 不支持跨语言调用,性能差,存在安全问题。

5、常见序列化协议

Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。

十二、I/O流

1、定义

IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。

Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

2、I/O 流为什么要分为字节流和字符流呢?

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

个人认为主要有两点原因:

  • 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
  • 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。

3、BIO/AIO/NIO

  • BIO同步阻塞IO模型:只要有连接就启动线程。
  • NIO同步非阻塞IO模型:多路复用器轮询道IO请求才启动线程。
  • AIO异步IO模型:操作系统完成后再通知服务器启动线程。

img

5、常见序列化协议

Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。

十二、I/O流

1、定义

IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。

Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

2、I/O 流为什么要分为字节流和字符流呢?

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

个人认为主要有两点原因:

  • 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
  • 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。

3、BIO/AIO/NIO

  • BIO同步阻塞IO模型:只要有连接就启动线程。
  • NIO同步非阻塞IO模型:多路复用器轮询道IO请求才启动线程。
  • AIO异步IO模型:操作系统完成后再通知服务器启动线程。

img

img

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值