您现在的位置 :

首页  >  市场分析 >  > 正文

Java-面试-八股文-笔记(持续更新)

时间 :2023-04-23 03:53:34   来源 : 哔哩哔哩

4月马上过去了,还没有找到暑假实习,先更新一下复习资料,努力找工作。

Java 和 C++ 的区别

Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:

Java 不提供指针来直接访问内存,程序内存更加安全


(资料图片)

Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。

Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。

C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。

continue、break 和 return 的区别是什么

continue :指跳出当前的这一次循环,继续下一次循环。

break :指跳出整个循环体,继续执行循环下面的语句。

return 用于跳出所在方法,结束该方法的运行。

成员变量

Java中的成员变量包括实例变量和静态变量。

实例变量是属于对象的变量,每个对象都有自己的一份实例变量,它们在内存中存储在对象的内部。实例变量在对象被创建时被初始化,并且每个对象的实例变量值是相互独立的。实例变量通常用于存储对象的状态信息,比如一个人的姓名、年龄、性别等。实例变量只能通过对象来访问,不能通过类名来访问。

静态变量是属于类的变量,所有对象共享一份静态变量,它们在内存中只会有一份拷贝,无论该类被实例化多少次,该变量的值都是相同的。静态变量通常用于存储所有对象共享的数据,比如常量、全局计数器等。静态变量可以通过类名来访问,也可以通过对象来访问,但是建议使用类名来访问。

成员变量包括实例变量和静态变量,它们都有默认值。对于基本数据类型的成员变量,默认值为0或false,对于对象引用类型的成员变量,默认值为null。

成员变量的访问权限可以通过访问修饰符来控制,常用的访问修饰符包括public、private、protected和默认访问修饰符。默认访问修饰符表示只有同一包中的类可以访问该成员变量。

需要注意的是,在Java中,成员变量是通过类中的构造方法来初始化的。当一个对象被创建时,它的成员变量都会被初始化为其对应的默认值,然后再根据构造方法中的初始化语句进行初始化。

面向对象的三大特性

封装、继承、多态

1、封装

把对象的属性操作等隐藏起来,保留对外的接口,用户通过接口来访问这个对象。

优点:

提高安全性

可重用

减少耦合

2、继承

继承就是子类继承父类的特征和行为。

子类继承父类的所有属性和方法,但是父类中的私有方法子类无法访问,只是拥有。

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

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

Java不支持多重继承,但一个类可以实现多个接口,从而克服单继承的缺点;

优点:

通过继承可以快速创建新的类,实现代码的重复利用,提高开发的效率。

super关键字使用super可以访问父类成员属性,成员方法,构造方法。super.父类成员变量super.成员方法名();super();//父类构造方法

访问父类的构造函数: 可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。

访问父类的成员: 如果子类重写了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。

3、多态

在 Java 中,多态性通常通过继承和方法重写实现。当子类继承了父类并且重写了父类的方法时,如果使用父类引用指向子类对象,并调用该方法,则实际上调用的是子类的实现。这就是多态性的体现。

多态是指同一行为,对于不同的对象具有多个不同表现形式:当一个子类继承了父类并且重写了父类的方法时这个方法可以在不同的子类对象上表现出不同的行为,即同一个方法调用可以有不同的实现方式。

多态分为编译时多态和运行时多态:

编译时多态主要指方法的重载

编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法

运行时多态 指 程序中定义的 对象引用所指向的具体类型 在运行期间才确定

运行时多态有三个条件:

1、继承 2、重写 3、向上转型

向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。父类对象的引用指向子类对象。

只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。

在 Java 中,一个类可以继承另一个类。父类通常具有一些共性的属性和方法,子类可以继承父类的这些属性和方法,并且可以添加自己特有的属性和方法。因此,子类既拥有父类的属性和方法,也有自己特有的属性和方法。

在 Java 中,一个父类的引用变量可以指向一个子类的对象。这就意味着,可以使用一个父类的引用变量来调用子类中继承或者重写的方法。这种使用父类引用变量调用子类中的方法的方式,称为父类引用指向子类对象,并调用该方法。

例如,假设有一个父类 Animal 和两个子类 Dog 和 Cat,它们都重写了父类的 eat() 方法。现在可以创建一个 Animal 类型的引用变量 animal,然后让它指向一个 Dog 或 Cat 对象。这样,当调用 animal.eat() 方法时,程序会自动调用 Dog 或 Cat 类中重写的 eat() 方法,这就是父类引用指向子类对象,并调用该方法的实现方式。

这种使用父类引用变量指向子类对象并调用子类中的方法的方式,可以使代码更加灵活和可扩展,也可以提高程序的可读性和可维护性。

缓存池

是一种降低磁盘访问的机制。在Java中,常见的缓存池包括字符串缓存池、数字缓存池、日期时间缓存池等。

1、字符串缓存池

在Java中,字符串常量池是一个特殊的缓存池,用于存储字符串常量。当我们创建一个字符串常量时,如果该字符串已经存在于常量池中,则会直接返回该字符串的引用,否则会在常量池中创建该字符串并返回引用。这样可以避免创建大量相同的字符串对象,节省内存空间。

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

例如:

String str1 = "hello"; String str2 = "hello"; String str3 = new String("hello"); System.out.println(str1 == str2); // true,因为两个字符串都在字符串常量池中 System.out.println(str1 == str3); // false,因为str3是通过new关键字创建的,存储在堆内存中

2、数字缓存池

在Java中,为了提高程序的性能,会预先缓存一些常用的数字,如-128127之间的整数、0127之间的bytechar类型的数据等。当程序需要使用这些数字时,会直接从缓存池中获取,而不是重新创建一个对象。这样可以减少对象的创建次数,提高程序的性能和效率。

例如:

Integer i1 = 10; Integer i2 = 10; System.out.println(i1 == i2); // true,因为10在数字缓存池中 Integer i3 = 128; Integer i4 = 128; System.out.println(i3 == i4); // false,因为128不在数字缓存池中

new Integer(123) 与 Integer.valueOf(123) 的区别在于: new Integer(123) 每次都执行会新建一个对象 Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。

1.Integer的缓存池大小为一个字节,也就是(-128~127)。

2.valueOf()方法会首先判断参数是否存在于缓存池对象中,若存在,则引用并返回该对象,引用同一个对象时,它们的地址是一样的,因此用==去比较,则返回结果为true。

3.若参数不能在缓存池中找到对应对象,则会new一个包装类对象,每次new的对象地址是不同的,因此用==去比较它们返回false。

缓存池(Cache Pool)指的是在程序运行时预先缓存一些对象或数据,以便在需要时可以快速地访问,从而提高程序的性能和效率。

3、日期时间缓存池

在Java中,日期时间类(如DateCalendar等)的创建是比较耗费资源的,因此,Java提供了一个日期时间缓存池,用于存储一些常用的日期时间对象,以便在需要时可以直接从缓存池中获取。

例如,SimpleDateFormat类就使用了一个日期时间缓存池,用于缓存一些常用的日期时间格式,以便在格式化日期时间时可以直接从缓存池中获取。

包装类

在Java中,每种基本数据类型都有对应的包装类。包装类是一种特殊的类,用于将基本数据类型转换为对象,以便在需要对象的地方使用基本数据类型。

Java的基本数据类型有8种,每种基本数据类型都有对应的包装类,分别是:

byte    Byte

short    Short

int    Integer

long    Long

float    Float

double    Double

char    Character

boolean    Boolean

包装类与基本数据类型之间的转换可以通过自动装箱(Autoboxing)和自动拆箱(Unboxing)来完成。

自动装箱是指将基本数据类型自动转换为包装类对象,自动拆箱是指将包装类对象自动转换为基本数据类型

例如:

int num1 = 100; Integer num2 = num1; // 自动装箱,将int类型转换为Integer类型 

//将num1赋值给num2时,发生了自动装箱的过程, 

//即将num1的值转换为对应的Integer对象,所以num2的类型就是Integer。 

int num3 = num2; // 自动拆箱,将Integer类型转换为int类型

包装类除了能够将基本数据类型转换为对象,还可以提供一些实用的方法。例如,Integer类提供了许多静态方法,如parseInt()valueOf()等,用于将字符串转换为整数。另外,包装类还可以用于表示空值,例如在Java中,可以使用Integer对象来表示空值,其值为null

需要注意的是,使用包装类对象与基本数据类型的性能相比可能会有一些损失。因此,在进行大量计算或需要高效性能的场合,建议使用基本数据类型。

String

String 类型被声明为final ,不可以被继承;String字符串是不能被改变的。

String不可以改的原因:

1、内部使用char[]存储数据,该数组被声明为final

2、String内部没有提供更改value数组的方法

String不可变的好处

可以缓存 hash 值 (String 用做 HashMap 的 key)

String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool

c.安全性:线程安全,可以在多个线程中安全地使用

String, StringBuffer and StringBuilder 对比

1、String 不可变 | StringBuffer、StringBuilder 可变

2、String线程安全 | StringBuffer线程安全(内部使用synchronized进行同步)、 StringBuilder线程不安全。

StringBuilder和StringBuffer类提供了一些常用的方法,例如append()方法用于将一个字符串添加到另一个字符串的末尾,insert()方法用于将一个字符串插入到另一个字符串的指定位置,delete()方法用于删除字符串中的一部分,replace()方法用于替换字符串中的一部分等等。

StringBuffer和StringBuilder的底层都是使用字符数组(char[])来存储字符串的。

StringBuffer中,字符串是通过一个字符数组(char[])来存储的,并且在对字符串进行修改时,StringBuffer会自动调整字符数组的大小,以便存储更多的字符。由于StringBuffer是线程安全的,所以在对它的方法进行操作时,会使用synchronized关键字来保证线程安全。这使得StringBuffer的性能会比较低,适合在多线程的环境下使用。

StringBuilder中,字符串也是通过一个字符数组(char[])来存储的,但是它是非线程安全的,因此在对它进行修改时,不会进行同步操作,这使得StringBuilder的性能会比较高,适合在单线程的环境下使用。

对字符串进行修改时,StringBuffer和StringBuilder都会进行以下操作:

1、如果要修改的字符串长度超过了字符数组的长度,则会创建一个新的字符数组,并将原来的字符串复制到新的字符数组中。

2、如果要修改的字符串长度不超过字符数组的长度,则直接在原来的字符数组中修改字符串。

3、如果要在字符串中插入一个新的字符,则需要将插入位置后面的字符向右移动。

String.intern()

使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。(指向同一个对象)

String s1 = new String("aaa"); 

String s2 = new String("aaa"); 

System.out.println(s1 == s2);           // false String s3 = s1.intern(); System.out.println(s1.intern() == s3);  // true 

//intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。 //因此 s3 和 s1 引用的是同一个字符串常量池的对象。 

String s4 = "bbb"; String s5 = "bbb"; 

System.out.println(s4 == s5);  // true 

//如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。

字符串拼接使用+还是使用Stringbuilder

都可以,字符串使用+进行拼接时是通过StrigBuilder调用append()实现,拼接完成后调用toString().

但是如果在循环内部使用 + 拼接,每循环一次就会创建一个StringBuilder对象。

直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。

String[] arr = {"he", "llo", "world"}; 

StringBuilder s = new StringBuilder(); //只有一个StringBuilder对象 

for (String value : arr) {    

s.append(value);

System.out.println(s);

String中的equals() 和 Object中的equals()的区别?

String 中的 equals 方法是被重写过的,比较的是 String 字符串的各个字符是否相等。

Object 的 equals 方法是比较的对象的内存地址。

参数传递

Java 的参数是以值传递的形式传入方法中,而不是引用传递。

实参(实际参数,Arguments):用于传递给函数/方法的参数,必须有确定的值。

形参(形式参数,Parameters):用于定义函数/方法,接收实参,不需要有确定的值。

值传递方法接收的是实参值的拷贝,会创建副本 (Java)

引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。(C++)

抽象类和接口

1、抽象类与抽象方法:

抽象类和抽象方法都使用abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。

抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。

抽象类主要用于代码复用,强调的是所属关系。

2、抽象类与接口

共同点:

都不能实例化,必须由子类来实现它们的抽象方法。

都可以被子类继承或实现。

都可以包含抽象方法。

都可以用于实现多态。

不同点:

实现方式不同:抽象类是通过继承来实现的,而接口是通过实现(implements)来实现的。

方法实现方式不同:抽象类可以包含抽象方法和非抽象方法的实现,而接口只能包含抽象方法的声明,没有方法的实现。

多继承问题不同:Java中的类只能单继承,但是可以实现多个接口,因此接口可以用来实现多重继承,而抽象类只能单继承。

访问修饰符不同:接口中的方法默认是public的,而抽象类中的方法可以有不同的访问修饰符

成员变量不同:接口中的成员变量默认是public static final的,而抽象类中可以有各种类型的成员变量

一个类只能继承一个类,但是可以实现多个接口

接口中的成员变量只能是 public 、static 、final 类型的不能被修改且必须有初始值,

而抽象类的成员变量默认 protected/private可在子类中被重新定义,也可被重新赋值

接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为

抽象类主要用于代码复用,强调的是所属关系。

重写与重载

1、重写

存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。子类重写了父类的方法。

重写的要求:

1、子类方法的访问权限>= 父类

2、子类方法的返回类型必须 是 父类方法的返回类型或其子类型(<=父类的返回类型)

总结:

方法名、参数列表必须相同,返回值范围<=父类,抛出的异常<=父类,访问修饰符范围>=父类,如果父类是private修饰的方法,子类不能重写。

使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。

2、重载

同一个类中,一个方法与另一个方法名字相同,但是参数类型、个数、顺序至少有一个不同

应该注意的是,返回值不同,其它都相同不算是重载。

使用的是@OverLoad注解

equals() 与 ==

1、equals等价关系

1、自反性  2、对称性 3、传递性 4、一致性

//自反性

x.equals(x); // true 

//对称型x.equals(y) == y.equals(x); // true 

//传递性if (x.equals(y) && y.equals(z))    

x.equals(z); // true; 

//一致性 多次调用 equals() 方法结果不变 

x.equals(y) == x.equals(y); // true 

//与 null 的比较 对任何不是 null 的对象 x 调用 

x.equals(null) 结果都为 falsex.equals(null); // false;

2、 == 与 equals()

对于基本类型,== 判断两个值是否相等基本类型没有 equals() 方法。

对于引用类型== 判断两个变量是否引用同一个对象(对象的内存地址),而 Object类中的equals() 在没有重写equals方法之前,equals方法里是直接调用==,因此实质上与==没有差别。

String 中的 equals 方法是被重写过的,比较的是 String 字符串的各个字符是否相等。 Object 的 equals 方法是比较的对象的地址。

Java泛型

Java泛型是Java 5引入的一个新特性,它允许我们定义泛化的类、接口和方法,从而可以在编译时检查类型安全,避免了类型转换异常等问题。

泛型的本质是参数化类型,它将类型作为参数传递给类或方法,使得代码可以更加通用和灵活。

泛型的好处包括:

类型安全:泛型可以在编译时进行类型检查,避免了在运行时由于类型转换错误而导致的异常。

代码复用:使用泛型可以将代码写得更加通用,从而减少了重复代码的数量。

可读性:使用泛型可以使代码更易于理解和维护,因为它可以使代码更加简洁和易读。

Java泛型的实现方式是通过类型擦除来实现的。在编译时,Java编译器将泛型转换为原始类型,即将泛型中的类型参数替换为Object类型,并插入必要的类型转换代码。这样做的好处是可以使Java泛型与Java 5之前的版本兼容,并且减少了对Java虚拟机的影响。

具体来说,Java泛型的实现方式包括以下几个方面:

类型擦除:Java泛型在编译时会将泛型类型转换为原始类型,并插入必要的类型转换代码,这个过程称为类型擦除。

泛型类和泛型方法:Java泛型可以定义泛型类和泛型方法,其中泛型类是指使用泛型类型参数的类,泛型方法是指使用泛型类型参数的方法。

通配符类型:Java泛型还支持通配符类型,它可以用于表示一组具有相同类型上限的类型。

泛型接口:Java泛型也可以用于接口中,使得接口可以定义泛型类型参数。

总的来说,Java泛型是Java语言中非常重要的一个特性,它使得代码更加通用、安全、易读和易维护。

hashcode()

1、什么是hashcode?

HashCode是Java中的一个方法,它用于返回对象的哈希码,即对象在内存中的地址经过哈希算法处理后得到的值。在Java中,哈希码主要用于哈希表等数据结构中,可以快速地定位对象。HashCode方法是Object类中的一个方法,因此所有的Java对象都具有该方法。HashCode方法的默认实现是将对象的内存地址转换成一个整数值。

2、hashcode和equals()区别

equals() 是用来判断两个对象是否相等

hashcode 也是用来两个对象是否相等,但是判断结果不准确,因为哈希码相等的两个对象不一定是同一个对象。

相等的对象,hashcode一定相等。但是 hashcode相等的对象不一定相等。

所以equals()判断两个对象是否相等更加准确。

3、为什么 JDK 还要同时提供这两个方法呢?(提高效率)

在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高。

在Java的⼀些集合类的实现中,比较两个对象是否相等时先调用对象的 hashCode()方法得到hashCode进行比较如果hashCode不相同,就可以直接认为这两个对象不相同如果hashCode相同进⼀步调用equals()方法进行比较。

而equals()⽅法,就是⽤来最终确定两个对象是不是相等的因为equals()方法的实现会比较重逻辑⽐较多,⽽hashCode()主要就是得到⼀个哈希值,实际上就⼀个数字,效率较快,所以在比较两个对象时,通常都会先根据 hashCode先比较⼀下。

4、为什么不同的对象可能会有相同的hashcode/为什么产生哈希冲突

这种现象是哈希冲突。Hash叫做哈希表,就是把任意长度的输入,经过散列算法,变成固定的输出,哈希表的空间通常小于输入的空间,不同的输入可能会造成散列成相同的输出。即不同的对象有相同的hashcode,这就是哈希冲突。

5、如何解决Hash冲突:

1、开放地址法:

如果发生了哈希冲突,发现了这个地址被其他元素占用,那就寻找别的空的地址。寻找空的地址的方法可以是 :

1、线性探查法:从当前占用的地址往后,逐一排查,有空的地址就占用。如果查到表尾还没有就返回到表头,直到查完。

2、平方探查法:从发生冲突的位置d[i],按照地址+平方往后找,即d[i]+1,d[i]+22,d[i]+32直到找到空地址。

3、双散列函数探查法:使用两个散列函数

4、伪随机探查法:使用伪随机数来生成探查序列

2、链地址法:

将hashcode相同的元素构成一个同义词的单向链表,并将单向链表的头指针放在哈希表的第i个位置,查找、插入、删除都在这个同义词链表中进行。

链地址法(Chaining)是一种常见的解决哈希冲突的方法。它的基本思想是将哈希表中的每个槽(slot)都设为一个链表(linked list),哈希值相同的元素会被放在同一个槽对应的链表中。

当我们需要在哈希表中查找某个元素时,首先计算它的哈希值,然后找到对应的槽,最后遍历该槽对应的链表,直到找到目标元素或者遍历完整个链表。

当向哈希表中添加元素时,我们也可以使用链地址法。首先计算元素的哈希值,然后找到对应的槽,最后将元素插入该槽对应的链表的末尾。

如果哈希表的槽数足够大,那么链地址法可以有效地解决哈希冲突的问题,并且具有较好的查找、插入、删除操作的平均时间复杂度。但是当哈希表的槽数目相对较小,链表中的元素过多时,链地址法的效率就会降低,甚至可能退化成链表查找。因此,在设计哈希表时,需要根据实际情况选择合适的哈希函数和槽数目,以及采用适当的冲突解决方法,以提高哈希表的效率和性能。

什么是哈希表的樔?

可能您想问的是哈希表的“桶”(bucket),也称为哈希表的“槽”(slot)。哈希表是一种使用哈希函数将键映射到索引位置的数据结构,每个索引位置上可以存储多个键值对,这些键值对被存储在哈希表的桶或槽中。桶或槽可以理解为哈希表内部用于存储数据的一个位置,哈希函数会将键映射到某个桶或槽中,并在该位置上存储键值对。哈希表的性能很大程度上取决于桶的数量以及哈希函数的设计。

哈希表负载因子

哈希表的负载因子(load factor)是指哈希表中存储的键值对数量与哈希表槽数量的比值。通常用字母符号 alpha表示,计算公式为:

α=n/m其中 n$表示哈希表中键值对的数量,m$表示哈希表中槽的数量。

负载因子反映了哈希表的密集程度。当负载因子较小时,哈希表中空槽较多,可能会浪费一些存储空间;当负载因子较大时,哈希表中的槽可能会被填满,导致哈希冲突的概率增加,查找、插入、删除等操作的时间复杂度可能会变高。

通常情况下,哈希表的负载因子会设置一个阈值,当负载因子超过这个阈值时,就会对哈希表进行扩容。扩容通常包括增加槽的数量,并将已有的键值对重新映射到新的槽中,这样可以降低哈希冲突的概率,同时也可以提高哈希表的性能。

6、为什么重写 equals() 时必须重写 hashCode() 方法?

因为两个相等的对象的 hashCode 值必须是相等。

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

总结

如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。

如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。

如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。

深拷贝和浅拷贝

1、深拷贝

拷贝的对象和原始对象引用不同的对象。拷贝时会完全复制这个对象。

深拷贝则是将对象及其所有相关的对象都完全复制一份,形成一份全新的对象,新对象和原对象不共享内存区域。深拷贝需要遍历所有相关对象,并复制它们的值,所以可能比较耗时,但可以避免意外的副作用。

2、浅拷贝

拷贝的对象和原始对象共用同一个对象。

浅拷贝是将对象的引用(地址)复制一份,让新对象也指向原来对象所在的内存地址。这样,新对象和原对象会共享同一个内存区域,修改一个对象会影响到另一个对象。浅拷贝一般比较快,但是可能会造成意外的副作用。

浅拷贝适用于简单对象,而深拷贝适用于需要完全独立的对象。具体选择哪种拷贝方式,需要根据具体的应用场景和需求来决定。

Final、Static、this、supper

1、Final

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

final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;

final 修饰的方法不能被重写;

final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。

使用 final 方法的原因有两个:

把方法锁定,以防任何继承类修改它的含义;

效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。

2、Static

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

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

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

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

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

3、this关键字

this 关键字用于引用类的当前实例。 使用此关键字可能会使代码更易读或易懂。

4、super

super 关键字用于从子类访问父类的变量和方法。

public class Super {    protected int number;    protected showNumber() {        System.out.println("number = " + number);    }}public class Sub extends Super {    void bar() {        super.number = 10;        super.showNumber();    }}

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

在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。

this、super 不能用在 static 方法中。

原因:

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

反射

1、什么是反射,它的作用是什么?

Java 反射是指在运行时动态地获取类的信息并操作类的成员变量、方法和构造方法等。

Java 反射的作用是增强程序的灵活性和扩展性。

反射是框架的灵魂,在运行状态中,通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。反射就是把java类中的各种 成分 映射成一个个的Java对象,例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。

Java 反射主要涉及以下三个类:

Class 类:表示一个类或一个接口的运行时对象,可以获取类的信息和创建类的实例。

Constructor 类:表示类的构造方法,可以通过 Constructor 类创建类的实例。

Method 类:表示类的方法,可以通过 Method 类调用类的方法。

2、反射与泛型如何配合使用?

反射和泛型可以配合使用,可以通过反射获取泛型类型、泛型类和泛型方法等信息。

3、如何通过反射获取一个类的构造方法、属性和方法?

可以使用反射 API 中的 Class 类、Constructor 类、Field 类和 Method 类等,分别表示类、构造方法、属性和方法,来获取类的构造方法、属性和方法。

4、如何通过反射创建一个对象?

可以通过调用 Class 对象的 newInstance() 方法,使用构造方法来创建一个对象。

5、反射和类的加载、实例化、初始化有什么关系?

在运行时,Java 虚拟机将类加载到内存中,然后根据需要实例化对象并初始化。

6、什么是反射中的访问权限检查,如何绕过?

反射中的访问权限检查指的是在通过反射调用类的私有成员时会触发的安全检查。可以通过设置 setAccessible(true) 绕过访问权限检查。

7、反射和注解之间有什么联系?

可以通过反射获取注解,并根据注解信息进行相应的处理。

8、什么是动态代理,如何使用反射实现动态代理?

动态代理是指通过反射在运行时动态地生成代理类,实现对目标对象的代理操作。可以使用 Proxy 类和 InvocationHandler 接口来实现动态代理。

9、什么是反射中的 Class 对象,有什么作用?

反射中的 Class 对象是表示一个类的类型信息,可以使用 Class 类中的方法获取类的信息,如类名、包名、父类、接口、构造方法、属性和方法等信息。

10反射和性能有什么关系?反射调用方法和普通调用方法的性能差距有多大?

反射调用方法的性能较普通调用方法要差,主要原因是反射调用方法需要进行方法查找和类型转换等操作,而这些操作需要花费额外的时间。在使用反射调用方法时,可以通过缓存 Class 对象和 Method 对象来提高性能。

11、反射机制执行的流程

反射实现的原理

反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;

每个类都会有一个与之对应的Class实例,从而每个类都可以获取method反射方法,并作用到其他实例身上;

反射也是考虑了线程安全的,放心使用;

反射使用软引用relectionData缓存class信息,避免每次重新从jvm获取带来的开销;

反射调用多次生成新代理Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;

当找到需要的方法,都会copy一份出来,而不是使用原来的实例,从而保证数据隔离;

调度反射方法,最终是由jvm执行invoke0()执行;

12、反射的底层原理

反射的底层原理涉及到Java虚拟机(JVM)的类加载机制和字节码的动态生成和执行等技术。

在Java程序运行时,JVM会将类加载到内存中,并生成对应的Class对象来表示这个类的类型信息。反射通过获取这个Class对象,可以在运行时动态地操作类的构造方法、属性和方法等信息。

当使用反射调用类的方法时,JVM会将字节码转换成机器码来执行方法。反射机制通过生成MethodHandle对象来直接调用方法,避免了字节码转换成机器码的过程,从而提高了调用方法的效率。

另外,在动态代理中,反射可以通过生成代理类的字节码来实现对目标对象的代理操作。生成代理类的字节码使用Java字节码操作库来完成,可以通过ASM、Javassist等工具来实现。

总之,反射的底层原理是利用JVM的类加载机制、字节码动态生成和执行等技术来实现对类的动态操作,从而增强了程序的灵活性和扩展性。

异常

1、异常的层次结构

Throwable 是 Java 语言中所有错误与异常的超类。

Throwable 包含两个子类:Error(错误)和 Exception(异常)

2、Error(错误)

Error 类及其子类:程序中无法处理的错误, 此类错误一般表示代码运行时 JVM 出现问题。

通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。

此类错误发生时,JVM 将终止线程。

这些错误是不受检异常,非代码性错误。

3、Exception(异常)

程序本身可以捕获并且可以处理的异常。可以通过 catch 来进行捕获。

Exception 这种异常又分为两类:运行时异常(不受检查异常)和编译时异常(受检查异常)。

1、运行时异常/不受检查异常

RuntimeException类及其子类异常。

NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)、ClassCastException(类型转换错误)等。

这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理

这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。

2、编译时异常/受检查异常

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。

从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOExceptionSQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

4、异常的关键字

try– 用于监听。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。

catch– 用于捕获异常。catch用来捕获try语句块中发生的异常。

finally– finally语句块总是会被执行。

无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

throw– 用于抛出异常。

throws– 用在方法签名中,用于声明该方法可能抛出的异常。

注意:不要在 finally 语句块中使用 return!当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

5、异常的抛出

如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。如下所示:

public static double method(int value) {    

if(value == 0) {        

throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常    

}    

return 5.0 / value;

}

有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。

private static void readFile(String filePath) throws MyException {       

try {        // code   

} catch (IOException e) {        

MyException ex = new MyException("read file failed.");       

ex.initCause(e);       

throw ex;    

}

习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用), 比如下面用到的自定义MyException:

public class MyException extends Exception {    

public MyException(){ }    

public MyException(String msg){        

super(msg);    

}    

// ...

}

6、异常基础总结

try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally。

try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理。

finally语句块中的代码一定会被执行,常用于回收资源 。

throws:声明一个异常,告知方法调用者。

throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。

7、什么是异常链?如何在Java中实现异常链?

异常链是指一个异常对象中包含另一个异常对象的引用,这样的话就可以在异常处理的过程中把多个异常链接起来,形成一个异常链,从而更好地描述程序的异常情况。在Java中,可以通过在构造函数中传递一个cause参数来实现异常链。

线程和进程的区别

线程是操作系统能够调度的最小单位。是进程的实际运作单位。

进程是计算机正在运行的一个程序实例。比如打开微信。

一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈。

进程与进程之间相互独立,但线程不一定,线程与线程可能会相互影响。

线程的开销小,但不利于资源的管理与保护;进程开销大,但利于资源的管理与保护。

有了进程为什么还要先线程

1、进程切换开销比较大,线程切换成本低。

2、线程更轻量,一个进程可以创建多个线程。

3、多个线程可以并发处理不同的任务,高效利用处理器。

4、同一个进程内的线程共享内存和文件,他们之间相互通信无需调用内核。

进程有哪几种状态

1、创建:进程正在被创建,尚未到就绪状态

2、就绪:进程已处于准备运行状态,一旦得到处理器资源(处理器分配的时间片)即可运行。

3、运行:进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。

4、阻塞:又称为等待状态,进程正在等待某一事件而暂停运行。即使处理器空闲,该进程也不能运行。

5、结束:进程正在从系统中消失。

线程有哪几种状态

NEW :初始状态,线程被创建,但是还没有调用start方法。

RUNNABLE:可运行状态,线程正在JVM中执行,但是有可能在等待操作系统的调度。

BLOCKED :阻塞状态,线程正在等待获取监视器锁。

WTING :等待状态,线程正在等待其他线程的通知或中断。

TIMED_WTING:超时等待状态,在WTING的基础上增加了超时时间,即超出时间自动返回。

TERMINATED:终止状态,线程已经执行完毕。

线程与协程的区别

1、协程就是你可以暂停执行的函数。协程拥有自己的寄存器上下文和栈

2、一个进程可以有多个线程,一个线程又可以有多个协程。

3、协程是由程序控制的,在用户态执行,能够大幅度提升性能。

4、多个协程是串行执行的,只能在一个线程内运行

5、线程进程都是同步机制,而协程则是异步。

6、线程是抢占式,而协程是非抢占式的。

线程使用的方式/如何创建线程

有三种使用线程的方法:1、实现Runnable接口;2、实现Callable接口;3、继承Thread类。

本质都是实现Runnable接口

实现接口比较好,因为Java不支持多继承,继承了Thread类就不能在继承别的类了。Java支持实现多个接口。

1、实现Runnable

需要实现 run() 方法。通过 Thread 调用 start() 方法来启动线程。

public class MyRunnable implements Runnable {    

public void run() {        

// ...    

}

public static void main(String[] args) {    

MyRunnable instance = new MyRunnable();    

Thread thread = new Thread(instance);    

thread.start(); 

}

2、实现Callable()

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public class MyCallable implements Callable<Integer> {    

public Integer call() {        

return 123;    

}

public static void main(String[] args) throws ExecutionException, InterruptedException {    MyCallable mc = new MyCallable();    

FutureTask<Integer> ft = new FutureTask<>(mc);    

Thread thread = new Thread(ft);    

thread.start();    

System.out.println(ft.get()); 

}

3、继承Thread类

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

public class MyThread extends Thread {    

public void run() {        

// ...    

}

public static void main(String[] args) {    

MyThread mt = new MyThread();    

mt.start(); 

}

为什么要使用多线程

计算机Cpu是多核的,多个线程可以同时运行,减少了开销,提高了效率

多线程是开发高并发系统的基础,可以提高系统的并发能力及性能。

一个进程中有多个线程,多线程能提高cpu利用率

如何保证线程安全

1、原子操作;2、volatile;3、锁;等等

1、原子操作

原子操作是指在执行过程中不能被中断的操作,要么全部完成,要么全部不完成。原子操作通常通过原子类来实现。

原子操作类提供了一种用法简单、性能高效、线程安全的更新变量的方式。

atomic包中提供了17个类,分为4种类型的原子更新方式,分别是:

原子更新基本类型、原子更新引用类型、原子更新属性、原子更新数组

无论原子更新哪种类型,都要遵循CAS“比较和替换”规则,即比较要更新的值是否等于期望值,如果是则更新,如果不是则失败。

什么是原子类

原子类是一种线程安全的类,可以在不使用显式锁的情况下实现原子性操作。原子类通常是基本类型的包装类,如 AtomicInteger、AtomicBoolean、AtomicLong 等,它们提供了一些原子性的操作,如原子增减、原子比较和交换等操作,能够保证多个线程并发访问时对共享变量的操作是线程安全的。原子类通常利用底层的 CAS(Compare And Swap)操作实现,CAS 是一种乐观锁机制,能够在不加锁的情况下实现线程安全。

在使用原子类时需要注意以下几点:

原子类通常用于仅仅包含一个变量的操作,对于复杂的数据结构,还是需要使用锁来保证线程安全。

原子类可以提高程序的并发性能,但是不能保证所有的原子性操作都能够成功,因此在使用时还需要进行错误处理。

原子类是线程安全的,但是在多个原子性操作之间不一定是原子的,因此需要使用同步机制来保证多个原子性操作的原子性。

总之,原子类是一种方便、高效、线程安全的类,可以在不使用显式锁的情况下实现原子性操作,是 Java 并发编程中常用的工具类之一。

2、volatile

可以保证共享变量在线程之间的可见性,即一个线程修改了该变量的值,其他线程能够立即看到该变量的最新值。

volatile是轻量级的synchronized,他在多处理器开发中保证了共享变量的可见性,从而保证单个变量读写时的线性安全。

什么是可见性问题?可见性是指一个线程修改了共享变量的值,而其他的线程却看不到。

实现可见性:当写一个volatile变量时,该线程本地内存中的共享变量的值会被立即刷新到主内存;读一个volatile变量时,该线程本地内存会被置为无效,迫使线程从主内存种读取共享变量。

volatile实现线程安全的原理:

volatile关键字能够保证被修饰变量在多线程环境下的可见性和有序性,进而实现线程安全。其实现线程安全的原理主要包括两个方面:内存可见性 和 指令重排序。

内存可见性

使用 volatile修饰的变量会强制线程将本地内存中的变量值刷新到主内存中,从而保证了变量的内存可见性。(当写一个volatile变量时,该线程本地内存中的共享变量的值会被立即刷新到主内存;读一个volatile变量时,该线程本地内存会被置为无效,迫使线程从主内存种读取共享变量。)这样,一个线程修改了 volatile变量的值,其他线程可以立即看到修改后的值,而不是使用自己工作内存中的旧值。

指令重排序

在 JVM 运行时,为了提高程序的执行效率,编译器和处理器可能会对指令进行重排序。然而,重排序可能会导致程序执行的结果与预期不符,进而引发线程安全问题。

使用 volatile关键字可以禁止指令重排序,保证了变量的操作顺序的正确性。具体来说,当一个线程访问 volatile变量时,会在其前面插入一个 Load屏障,保证该线程从主内存中读取最新的值;当一个线程修改 volatile变量时,会在其后面插入一个 Store屏障,保证该线程修改的值会立即写回到主内存中,从而禁止了指令重排序。

需要注意的是,虽然 volatile可以保证可见性和有序性,但是它并不能保证原子性。如果需要保证操作的原子性,可以使用 synchronized或者 java.util.concurrent.atomic包中提供的原子类。

3、锁

Java种加锁的方式有两种:1、synchronized关键字 2、Lock接口。

synchronized 没有考虑到超时机制、非阻塞形式;因此Java1.5 增加了Lock接口

Lock 支持响应中断、超时机制、支持以非阻塞方式获取锁、支持阻塞队列

原子类和volatile只能保证单个共享变量的线程安全,锁可以保证临界区内的多个共享变量的线程安全。

其他实现线程安全的方式:

1、无状态设计(在并发环境中不设置共享变量)

2、使用不可变对象(并发环境中设置共享变量的类型为只读,在共享变量前加Final 或String修饰)

3、减少锁的粒度:如果某个代码段的锁粒度过大,可以将其拆分为多个小的代码段,从而减少锁的粒度,提高并发性能。

4、本地存储,避免共享(可以使用 ThreadLocal 类来避免共享变量,每个线程独立地拥有一份变量副本,从而避免多线程并发访问共享变量带来的问题。)

标签:

推荐文章

X 关闭

X 关闭