# Java 语言基础

# Java中的基本数据类型

Java中数据类型分为基本数据类型和引用数据类型。这些基本数据类型的数据变量在声明之后就会立刻在栈上被分配内存空间。

基本类型:

基本类型 大小 默认值 最小值 最大值 包装类型
boolean ---- ---- ---- ---- Boolean
char 16bits \u0000 \u0000 \uffff Character
byte 8bits 0 -128(-2^7) +127(2^7-1) Byte
short 16bits 0 -32768(-2^15) +32767(2^15-1) Short
int 32bits 0 -2,147,483,648(-2^31) 2,147,483,647(2^31-1) Integer
long 64bits 0L -9,223,372,036,854,775,808(-2^63) 9,223,372,036,854,775,807(2^63-1) Long
float 32bits 0.0f IEEE 754 IEEE 754 Float
double 64bits 0.0d IEEE 754 IEEE 754 Double

类(默认值null)、接口(默认值null)、数组(默认值null)。

# Java中数组的初始化方式有哪几种

一维数组的声明

① type arrayName[];

② type[] arrayName;
1
2
3

一维数组的初始化

① type arrayName[] = {val1,val2,val3,...};

例如:int myArray[] = {1,2,3,4,5,6,7};

② type[] arrayName = new type[arrayLength];

例如:int[] myArray = new int[12];

③ type[] arrayName = new int[]{val1,val2,val3,...}

例如:int myArray[] = new int[]{1,2,3,4,5,6,7};
1
2
3
4
5
6
7
8
9
10
11

二维数组声明

① type arrayName[][];

② type [][] arrayName;

③ type[] arrayName[];
1
2
3
4
5

二维数组初始化

① type arrayName[][] = {{val1,val2,val3},{val1,val2,val3},{val1,val2,val3}};

例如:int myArray[][] = {{1,2,3},{4,5,6},{7,8,9}};

Java中二维数组的第二维长度可以不同,比如:int[][] myArray = {{1,2},{3,4,5}};

② 也可以这样子:

int[][] arr = new int[3][];
arr[0] = new int[2];
arr[1] = new int[3];
arr[2] = new int[5];

③ type[] arrayName[]{{val1,val2,val3},{val1,val2,val3},{val1,val2,val3}};

int[] myArray = new int[]{{1,2},{1,2,3},{1,2,3,4,5}};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

创建一个数字数组时,所有元素都初始化为0。boolean数组的元素会初始化为false。对象数组的元素则初始化为一个特殊值null,表示这些元素(还)未存放任何对象。 Arrays.toString(a)会返回一个包含数组元素的字符串。如果确实希望将一个数组的所有值拷贝到 一个新的数组中,就要使用Arrays类的copyof方法。要想对数值型数组进行排序,可以使用Arrays类中的sort方法。这个方法使用了优化的快速排序(QuickSort)算法。Java实际上没有多维数组,只有一 维数组。多维数组被解释为"数组的数组"。

# Java的三大特性

封装、继承、多态。

封装:封装指的是属性私有化,根据需要提供setter和getter方法来访问属性。即隐藏具体属性和实现细节,仅对外开放接口,控制程序中属性的访问级别。封装目的:增强安全性和简化编程,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员。

继承:继承是指将多个相同的属性和方法提取出来,新建一个父类。Java中一个类只能继承一个父类,且只能继承访问权限非private的属性和方法。 子类可以重写父类中的方法,命名与父类中同名的属性。继承目的:代码复用。

多态:多态可以分为两种,设计时多态和运行时多态。设计时多态:即重载,是指Java允许方法名相同而参数不同(返回值可以相同也可以不相同)。运行时多态:即重写,是指Java运行根据调用该方法的类型决定调用哪个方法,子类对父类的方法重新实现了一次。多态目的:增加代码的灵活度。

ps:如果再加一种,那就是抽象特性,万物皆对象。

# ==、equals和hashCode()的区别与联系

==等于比较运算符,如果进行比较的两个操作数都是数值类型,即使它们的数据类型不相同,只要它们的值相等,也都将返回true。如果两个操作数都是引用类型,那么只有当两个引用变量的类型具有父子关系时才可以比较,而且这两个引用必须指向同一个对象,才会返回true,即比较的是两个变量的内存地址。

equals()是Object基类提供的一个方法,Object中equals()方法的默认实现就是返回两个对象==的比较结果,但是equals()可以被重写。很多类重写了 equals 方法,比如String、Integer 等把它变成了值比较,所以一般情况下equals比较的是值是否相等。

hashCode()方法是从Object类中继承过来的,它也用来鉴定两个对象是否相等。Object类中的hashCode()返回对象在内存中地址转换的一个int值,如果没有重新hashCode()方法,任何对象的hashCode()方法都是不相等的。一般覆盖equals()方法的同时也要覆盖hashCode()方法。

# ★★★★★ String、StringBuffer和StringBuilder的区别与联系

String是字符串常量,字符串长度不可变,因为存放字符串的数组被声明为final,因此只能赋值一次,不可修改。因为在每次对String类型进行修改时,都会生成一个新的String对象,然后将指针指向新的String对象。

StringBuffer是字符串变量,有Synchronized修饰,所以是线程安全的可变字符串序列。使用StringBuffer类时,每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。如果要频繁对字符串内容进行修改,出于效率问题考虑最好使用StringBuffer。

StringBuilder是字符串变量,不是线程安全的。在其实现内部,StringBuilder对象是一个包含字符序列的变长数组。

基本使用原则:如果要操作少量的数据,用String;单线程操作大量数据,用StringBuilder;多线程操作大量数据,用StringBuffer。不要使用String类的"+"来频繁的拼接,因为那样的性能很差。

StringTokenizer是用来切分字符串的工具类。

# ★★★★★ Java多态的实现机制

多态是面向对象程序设计中代码重用的一个重要机制,它表示同一个操作作用在不同对象时,会有不同的语义,从而产生不同的结果。多态有两种表现形式:方法的重载和方法的覆盖。

方法的重载,是指同一个类中有多个同名的方法,但这些方法有着不同的参数,因此在编译时就可以确定到底调用哪个方法,它是一种编译时多态。重载可以被看做一个类中的方法多态性。

方法的覆盖,子类可以覆盖父类的方法,因此同样的方法会在父类和子类中有着不同的表现形式。在Java语言中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象。同样,接口的变量也可以指向其实现类的实例对象。程序调用的方法是在运行期才动态绑定(绑定指的是将一个方法调用和一个方法主体连接到一起),就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。通过这种动态绑定的方法实现了多态。由于只有在运行时才能调用哪个方法,因此通过方法覆盖实现的多态也可以被称为运行时多态

重载和覆盖的区别:覆盖是子类和父类之间的关系,是垂直关系;重载是同一个类中方法之间的关系,是水平关系。覆盖只能由一个方法或只能由一对方法产生关系;重载是多个方法之间的关系。覆盖要求参数列表相同;重载要求参数列表不同。在覆盖关系中,调用方法是根据具体对象的类型(对象对应存储空间类型)来决定的;而重载关系是根据调用时的实参列表和形参列表来选择具体方法的。

# 两个对象的hashCode()相同,则equals()也一定为true吗?

不一定。两个对象的 hashCode() 相同,equals() 不一定 true。比如在map中,hashCode() 相等,只能说明这两个键值对的哈希值相同,不代表这两个键值对相等。例如:

String str1 = "Aa";
String str2 = "BB";
System.out.println(String.format("str1: %d | str2: %d",str1.hashCode(),str2.hashCode()));
System.out.println(str1.equals(str2));

输出结果:
str1: 2112 | str2: 2112
false
1
2
3
4
5
6
7
8

# Java 中final关键字的作用是什么?

  • final 修饰的类叫最终类,不能被继承。
  • final 修饰的方法叫最终方法,不能被重写,但可以被继承。
  • final 修饰的变量叫常量,必须初始化,初始化之后值不能被修改。

# 接口、抽象类和普通类的区别与联系。

接口:是指一个方法的集合,接口中的所有方法都没有具体方法体,可以理解为一种特殊的类,里面全部是由全局常量和公共的抽象方法所组成。接口是解决Java无法使用多继承的一种手段,但是接口在实际中更多的作用是制定标准。接口必须使用 implements 来实现接口,接口不能有构造函数,接口中成员变量默认为 public static final 修饰,必须赋初值,不能被修改,其所有的成员方法默认使用 public abstract 修饰。接口可以被看做抽象类的变体,接口中的所有方法都是抽象的,可以通过接口间接地实现多重继承。

抽象类:拥有抽象方法(指没有方法体的方法,同时抽象方法还必须使用关键字abstract 做修饰)的类就是抽象类,抽象类要使用abstract关键字声明。抽象类也可以拥有非抽象的方法。抽象类的子类使用 extends 来继承,抽象类可以有构造函数。抽象类中成员变量默认 default,可在子类中被重新定义,也可被重新赋值,抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰。只要包含一个抽象方法的类就必须声明为抽象类,抽象类可以声明方法的存在而不去实现它,被声明为抽象的方法不能包含方法体。在实现时,必须包含相同的或者更低的访问级别(public→protected→private)。抽象类在使用的过程中不能被实例化,但是可以创建一个对象使其指向具体子类的一个实例。抽象类的子类为父类中的所有抽象方法提供具体的实现,否则它们也是抽象类。

接口与抽象类的相同点:

①都不能被实例化

②接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能被实例化。

接口与抽象类的不同的:

①接口中只有定义,其方法不能在接口中实现,只有实现接口的类才能实现接口中定义的方法,而抽象类可以有定义与实现,即其方法可以在抽象类中被实现。

②接口需要实现(implements),但抽象类只能被继承(extends)。一个类可以实现多个接口,但一个类只能继承一个抽象类,因此使用接口可以间接地达到多重继承的目的。

③接口强调特定功能的实现,其设计理念是"has-a"关系;而抽象类强调所属关系,其设计理念为"is-a"关系。

④接口中定义的成员变量默认为public staticfinal,只能够有静态的不能被修改的数据成员,而且必须给其赋值,其所有成员方法都是public、abstract的,并且只能被这两个关键字修饰。而抽象类可以有自己的数据成员变量,还可以有非抽象的成员方法,抽象类中的成员变量默认为default(本包可见),当然也可以被定义为private、protected和public,这些成员变量可以在子类中被重新定义,也可以被重新赋值,抽象类的抽象方法不能用private、static、synchronized、native等访问修饰符修饰,同时方法必须以分号结尾,并且不带花括号。当功能需要累积时,用抽象类;不需要累积时,用接口。

⑤接口被用于实现比较常用的功能,便于日后维护或者添加删除方法;而抽象类更倾向于充当公共类的角色,不适用于日后重新对里面的代码进行修改。抽象类可以实现接口,抽象类也可以继承具体类。抽象类也可以有静态的main方法。

普通类:不能包含抽象方法,一个类只能继承一个抽象类,但可以实现多个接口。普通类可以直接实例化,抽象类不能直接实例化。

# Java对象初始化的顺序。

在Java语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建对象。

Java对象的初始化一般遵循三个原则(优先级依次递减)①静态对象(变量)优先于非静态对象(变量)初始化,其中静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次。②父类优于子类进行初始化。③按照成员变量的定义顺序进行初始化。即使变量定义散布于方法定义之中,它们仍然在任何方法(包括构造函数)被调用之前先初始化。

变量的初始化顺序如下:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。

# Java中的浅拷贝与深拷贝的区别。

浅拷贝:被拷贝的对象所有的变量都含有与原来对象相同的值,而所有对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅拷贝所考虑的对象,而没拷贝它所引用的对象。

深拷贝:被拷贝的对象所有变量都含有与原来对象相同的值,而那些引用其他的对象的变量将指向被拷贝的新对象,深拷贝把拷贝对象的引用也拷贝了。

当类中只有一些基本数据类型时,只需clone一下即可。但当类中包含一些对象时,就需要深拷贝了,实现方式是在对对象调用clone方法完成拷贝之后,接着对对象中的非基本类型的属性也调用clone方法完成深拷贝。

tips:Java在处理基本数据类型(比如int、char、double等)时,都是采用按值传递(传入的是参数的复制)的方式执行,除此之外的其他类型都是按照引用传递(传递的是对象的一个引用)的方式执行。

# 组合和继承的区别。

组合是在新类里创建原有类的对象,重复利用已有类的功能。继承是面向对象的主要特性之一,它允许设计人员根据其他类的实现来定义一个类的实现。组合和继承都允许在新的类中设置子对象,只是组合是显式的,而继承是隐式的。

除非两个类之间是"is-a"的关系,否则不要轻易地使用继承,不要单纯地为了实现代码的重用而使用继承,因为过多地使用继承会破坏代码的可维护性,当父类被修改时,会影响到所有继承自它的子类,从而增加了程序的维护成本和难度。

不要仅仅为了实现多态而使用继承,如果类之间没有"is-a"的关系,可以通过实现接口与组合的方式达到相同的目的。采用接口与组合的方式比采用继承的方式具有更好的可扩展性。

# Java的内部类有哪些?

静态内部类(static inner class)、成员内部类(member inner class)、局部内部类(local inner class)和匿名内部类。

静态内部类:是指被声明为static的内部类,它可以不依赖于外部类实例而被实例化,而通常的内部类需要在外部类实例化后才能实例化。静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法(包括私有类型)。

非静态内部类:一个静态内部类,如果去掉static关键字,就成为成员内部类。成员内部类为非静态内部类,它可以自由地引用外部类的属性和方法,无论这些属性和方法是静态的还是非静态的。但是它与一个实例绑定在了一起,不可以定义静态的属性和方法。只有在外部的实例被实例化后,这个内部类才能被实例化。非静态内部类中不能有静态成员。

局部内部类:指的是定义在一个代码块内的类,它的作用范围为其所在的代码块,是内部类中最少使用到的一种类型。局部内部类就像局部变量一样,不能被public、protected、private以及static修饰,只能访问方法中定义为final类型的局部变量。对一个静态内部类,去掉其声明中的static关键字,将其定义移入其外部类的静态方法或静态初始化代码中就成为了局部静态内部类。对于一个成员类,将其定义移入其内部类的实例方法或实例初始化代码中就成了局部内部类。局部静态内部类与静态内部类的基本特性相同。局部内部类与内部类的基本特性相同。

匿名内部类:匿名内部类是一种没有类名的内部类,不能用关键字class、extends、implements,没有构造函数,它必须继承其他类或实现其他接口。匿名内部类的好处是代码更加简洁紧凑,但带来的问题是易读性下降。使用匿名内部类遵守的原则:①匿名内部类不能有构造函数。②匿名内部类不能定义静态成员变量、方法和类。③匿名内部类不能是public、protected、private、static。④只能创建匿名内部类的一个实例。⑤一个匿名内部类一定是在new的后面,这个匿名类必须继承一个父类或实现一个接口。⑥因为匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。

# Java中final、finally和finalize的区别

final:用于声明属性、方法和类,分别表示属性不可变、方法不能被覆盖和类不可被继承(不能再派生出新的子类)。

final属性:被final修饰的变量不可变。不可变有两种含义:一是引用不可变,二是对象不可变。final指的是引用不可变,对象里面的内容还是可以变的。被final修饰的变量必须被初始化,有以下几种方式:①在定义的时候初始化;②final成员变量可以在初始化块中初始化,但不可以在静态初始化块中初始化;③静态final成员变量可以在静态初始化块中初始化,但不可在初始化块中初始化;④在类的构造函数中初始化,但静态final成员变量不可在构造函数中初始化。

final方法:当一个方法被声明为final时,该方法不允许任何子类重写这个方法,但子类仍然可以使用这个方法。

final参数:用来表示这个参数在这个函数中不允许被修改。

final类:当一个类被声明为final时,此类不能被继承,所有方法都不能被重写。但是这并不表示final类的成员变量也是不可变的,要想做到final类的成员变量不可改变,必须给成员变量增加final修饰。一个类不能既被声明为abstract,又被声明为final。

finally:作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定被执行,经常被用在需要释放资源的情况下。

finalize:是Object类的一个方法,在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其他资源的回收,例如关闭文件等。一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

# Java中static关键字有哪些作用?

static关键字主要有两种作用:第一,为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关。第二,实现某个方法或属性与类而不是对象关联在一起,也就是说,在不创建对象的情况下就可以通过类来直接调用方法或使用类的属性。static主要有四种使用情况:成员变量、成员方法、代码块和内部类。

①static成员变量

static静态变量属于类,而不是属于实例,在内存中只有一个复制(所有实例都指向同一个内存地址),只要静态变量所在的类被加载,这个静态变量就会被分配空间,因此也就可以被使用了。对静态变量的引用有两种方式,分别为"类.静态变量"和"对象.静态变量"。实例变量属于对象,只有对象被创建后,实例变量才会被分配空间,才能被使用,它在内存中有多个复制,只能用"对象.实例变量"的方式来引用。在Java语言中不能在方法中定义static变量。

②static成员方法

static方法是类的方法,不需要创建对象就可以被调用,而非static方法是对象的方法,只有对象被创建出来后才能被使用。static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和成员方法,因为当static方法被调用时,这个类的对象可能还没被创建,即使已经被创建了,也无法确定调用哪个对象的方法。同样,static方法也不能访问非static类型的变量。static一个很重要的用途是实现单例模式,即把构造函数声明为private,并提供一个创建对象的方法,由于构造对象被声明为private,外界无法直接创建这个类型的对象,只能通过该类提供的方法来获取类的对象,要达到这样的目的只能把创建对象的方法声明为static。用public修饰的static变量和方法本质上都是全局的,若在static变量前用private修饰,则表示这个变量可以在类的静态代码块或者类的其他静态成员方法中使用,但是不能在其他类中通过类名来直接引用。

③static代码块

静态代码块在类中是独立于成员变量和成员函数的代码块的。它不在任何一个方法体内,JVM在加载类时会执行static代码块,如果有多个static代码块,JVM将会按顺序来执行。static代码块经常被用来初始化静态变量。static代码块只会被执行一次。

④static内部类

static内部类是指被声明为static的内部类,它可以不依赖于内部类实例对象而被实例化,而通常的内部类需要在外部类实例化后才能实例化。静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法(包括私有类型)。

# Java中使用switch时有哪些注意事项?

switch语句多用于分支选择,在使用switch(expr)时,expr只能是一个枚举常量(内部也是由整型或字符类型实现)或一个整数表达式,其中整数表达式可以是基本类型int或对应的包装类型Integer,当然也包括不同的长整型,例如short。由于byte、short和char类型的值都能够被隐式地转换为int类型,因此这些类型以及它们对应的包装类型都可以作为switch的表达式。但是,long、float、double、String类型不能够隐式地转换为int类型,因此它们不能被作为switch的表达式。如果一定要使用long、float、double作为switch的参数,必须将其强制转换为int型才可以。case语句之后可以是直接的常量数值,也可以是一个常量计算式,还可以是final型的变量,但不能是变量或带有变量的表达式例如i*2等,更不能是浮点数类型。JDK7之后,switch开始支持String类型了,其原理是通过case后面的String对象调用hashCode()方法,得到一个int类型的值,然后用这个hash值来唯一标识这个case,同时switch的case字句中String变量不能为null。

# Java中strictfp关键字有什么作用?

关键字strictfp是strict floatpoint的缩写,指的是精确浮点,它用来确保浮点数运算的准确性。JVM在执行浮点数运算时,如果没有指定strictfp关键字,此时计算结果可能会不精确。用strictfp来声明一个类、接口或者方法,那么在声明的范围内,Java编译器以及运行环境会完全按照IEEE二进制浮点数算术标准(IEEE 754)来执行,在这个关键字声明的范围内所有浮点数的计算都是精确的。当一个类被strictfp修饰时,所有方法都会自动被strictfp修饰。因此,strictfp可以保证浮点数运算的精确性,而且在不同硬件平台上都会有一致的运行结果。在Java17中,再次要求虚拟机完成严格的64位运算,strictfp关键字现在已经过时了。

# Java中值传递和引用传递的区别。

值传递:在方法调用中,实参会把它的值传递给形参,形参只是用实参的初始化值初始化一个临时的存储单元,因此形参与实参虽然有着相同的值,但是却有着不同的存储单元,因此形参的改变不会影响实参的值。

引用传递:在方法调用中,传递的是对象(也可以看做是对象的地址),这时形参与实参的对象指向同一块存储单元,因此形参的修改会影响到实参的值。

在Java语言中,原始数据类型在传递参数时,都是按值传递,而包装类型在传递参数时是按引用传递的。Java8中处理8种基本的数据类型用的是值传递,其他所有类型用的都是引用传递。

# Java类型转换规则。

Java类型转换规则是从低精度向高精度转换,即优先级满足byte→short→char→int→long→float→double。低级数据类型可以自动转换为高级数据类型,当类型自动转换时,char类型的数据转换为高级类型(int、long等),会转换为其对应的ASCII码。

byte、char、short类型的数据在参与运算时会自动转换为int型,但当使用"+="运算时,就不会产生类型的转换,因为"+="为Java语言规定的运算法,Java编译器会对其进行特殊处理,因此语句short s = 1; s += 1;能够编译通过,而short s = 1; s = s + 1;则会报错。

在涉及byte、short、char类型的运算时,首先会把这些类型的变量强制转换成int类型,然后对int类型的值进行计算,最后得到的值也是int类型。因此,如果把两个short类型的值相加,最后得到结果是int类型;如果把两个byte类型的值相加,最后也会得到int类型的值。如果需要得到short类型的结果,就必须显式地把运算结果强转成short。

基本数据类型与boolean类型是不能互相转换的。

# char类型变量中是否可以存储一个中文汉字?

可以。在Java语言中,默认使用的Unicode编码方式,即每个字符占两个字节,因此可以用来存储中文。String是由char所组成的,但是它采用了一种更加灵活的方式来存储,即英文占用一个字符,中文占用两个字符,采用这种存储方式的一个重要作用就是可以减少所需的存储空间,提高存储效率。

# 运行时异常和普通异常的区别

Java提供了两种错误的异常类,分别为Error和Exception,他们拥有共同的父类Throwable。

Error表示程序在运行期间出了非常严重的错误,并且错误不可恢复,如OutOfMemoryError、ThreadDeath等。

Exception表示可以恢复的异常,是编译器可以捕捉到的。包含两种类型:检查异常和运行时异常。① 检查异常。Java编译器强制程序去捕获此类异常,如IO异常和SQL异常。② 运行时异常。编译器不会对其强制进行捕获并处理。如果不进行处理,出现异常时JVM会来处理。出现运行时异常,系统会把异常一直往上抛,直到遇到处理代码为止。若没有处理代码,则抛到最上层。多线程由Thread.run()方法抛出,单线程由main()方法抛出。如果不处理异常,一旦发生,要么线程终止,要么主程序终止。运行时异常包括:NullPointException(空指针异常)、ClassCastException(类型转换异常)、ArrayIndexOutOfBoundsException(数组越界异常)、ArrayStoreException(数组存储异常)、BufferOverflowException(缓冲区溢出异常)、ArithmeticException(运算输异常)。

# 什么是注解?什么是元注解?

注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override 标识一个方法是重写方法。

元注解是自定义注解的注解,例如:

@Target:约束作用位置,值是 ElementType 枚举常量,包括 METHOD 方法、VARIABLE 变量、TYPE 类/接口、PARAMETER 方法参数、CONSTRUCTORS 构造方法和 LOACL_VARIABLE 局部变量等。

@Rentention:约束生命周期,值是 RetentionPolicy 枚举常量,包括 SOURCE 源码、CLASS 字节码和 RUNTIME 运行时。

@Documented:表明这个注解应该被 javadoc 记录。

# JDK8 新特性有哪些?

**lambda 表达式:**允许把函数作为参数传递到方法,简化匿名内部类代码。

**函数式接口:**使用 @FunctionalInterface 标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。

**方法引用:**可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。

**接口:**接口可以定义 default 修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。

**注解:**引入重复注解机制,相同注解在同一地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。

**类型推测:**加强了类型推测机制,使代码更加简洁。

**Optional 类:**处理空指针异常,提高代码可读性。

**Stream 类:**引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach 遍历、count 统计个数、filter 按条件过滤、limit 取前 n 个元素、skip 跳过前 n 个元素、map 映射加工、concat 合并 stream 流等。

**日期:**增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。

**JavaScript:**提供了一个新的 JavaScript 引擎,允许在 JVM上运行特定 JavaScript 应用。

# 字符串拼接的方式有哪些?

① 直接用 + ,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 + 拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。

② 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用 getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +

③ 使用 StringBuilder 或 StringBuffer,两者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 确定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy 将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。

# String a = "a" + new String("b") 创建了几个对象?

常量和常量拼接仍是常量,结果在常量池,只要有变量参与拼接结果就是变量,存在堆。

使用字面量时只创建一个常量池中的常量,使用 new 时如果常量池中没有该值就会在常量池中新创建,再在堆中创建一个对象引用常量池中常量。因此 String a = "a" + new String("b") 会创建四个对象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。

# String s = "Honey"与String s = new String("Honey")一样吗?

不一样,内存分配方式不同。String s = "Honey"方式,Java VM会将其分配到常量池中;而String s = new String("Honey")则会被分配到堆内存中。

String的实现采用了Flyweight的设计模式,当创建一个字符串常量时,例如String s = "Honey",会首先在字符串常量池中查找是否已经有相同的字符串被定义,判断的依据是String类equals(Object obj)方法的返回值。若已经定义,则直接获取对其的引用,此时不需要创建新的对象;若没有定义,则首先要创建好这个对象,然后把它放到字符串常量池中,再将它的引用返回。对于赋值语句String s = null;,其中s是一个字符串类型的引用,它不指向任何一个字符串。而赋值语句 String s = "";中的s是一个字符串类型的引用,它指向另外一个字符串(这个字符串的值为"",即空字符串)。

# 类之间有哪些关系?

类关系 描述 权力强侧 举例
继承 父子类之间的关系:is-a 父类 小狗继承于动物
实现 接口和实现类之间的关系:can-do 接口 小狗实现了狗叫接口
组合 比聚合更强的关系:contains-a 整体 头是身体的一部分
聚合 暂时组装的关系:has-a 组装方 小狗和绳子是暂时的聚合关系
依赖 一个类用到另一个:depends-a 被依赖方 人养小狗,人依赖于小狗
关联 平等的使用关系:links-a 平等 人使用卡消费,卡可以提取人的信息

# 访问权限控制符有哪些?

访问权限控制符 本类 包内 包外子类 任何地方
public
protected ×
× ×
private × × ×

# ★★★★★ Java中静态代理和动态代理的区别

代理模式,使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。

静态代理: 由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。

动态代理类: 在程序运行时,运用反射机制动态创建而成。从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。

JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,实现JDK里的InvocationHandler接口的invoke方法,通过Proxy里的newProxyInstance得到代理对象,在调用具体方法前调用 InvokeHandler 来处理。

CGlib动态代理:以 CGLIB(Code Generation Library)的方式进行代理,它采用底层字节码技术。通过在运行时将代理对象类的 class 文件加载进来,通过修改其字节码生成子类来处理。

区别:JDK 代理只能对实现接口的类生成代理;CGLIB 是针对类实现代理,继承指定类并生成一个子类,因此不能代理 final 修饰的类。

最近更新: 12/3/2024
勤奋的凯尔森同学   |