Java基础面试题
解释下什么是面向对象?面向对象和面向过程的区别?
-
面向对象是一种基于面向过程的编程思想
-
把数据和操作数据的行为封装在一起,形成对象。
-
对象有自己的属性和方法,对象之间相互传递消息来进行交互。
-
通过定义类来实现代码的模块化、重用性和可维护性。
-
-
区别
-
编程思路不同:面向过程以实现功能的函数开发为主,而面向对象要首先抽象出类、属性及其方法,然后通过实例化类、执行方法来完成功能。
-
封装性:都具有封装性,但是面向过程是封装的是功能,而面向对象封装的是数据和功能。
-
面向对象具有继承性和多态性,而面向过程没有继承性和多态性,所以面向对象优势很明显
-
面向对象是一种编程思想和方法,它将数据和操作数据的行为封装在一起,形成对象。每个对象都拥有自己的状态(属性)和行为(方法),对象之间可以通过相互传递消息来进行交互。面向对象的编程强调通过定义类来实现代码的模块化、重用性和可维护性。
面向过程是一种以过程为中心的编程思想和方法,在此方法中,程序被设计为一系列的步骤或函数,这些函数按照先后顺序执行,以完成特定的任务。面向过程编程关注的是如何解决问题和组织数据,较为直接和线性。
下面分别用Python语言实现一个简单的面向对象和面向过程示例代码作为例子。
面向对象示例代码:
class Rectangle: def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height def perimeter(self): return 2 * (self.width + self.height) rect = Rectangle(5, 10) print(f"Rectangle area: {rect.area()}") print(f"Rectangle perimeter: {rect.perimeter()}")
上述代码中,定义了一个 Rectangle
类,类中包含了两个属性 width
和 height
和两个方法 area()
和 perimeter()
。通过封装这些数据和行为在一起,我们可以创建一个 Rectangle
对象来计算它的面积和周长。
面向过程示例代码:
def rectangle_area(width, height): return width * height def rectangle_perimeter(width, height): return 2 * (width + height) width = 5 height = 10 area = rectangle_area(width, height) perimeter = rectangle_perimeter(width, height) print(f"Rectangle area: {area}") print(f"Rectangle perimeter: {perimeter}")
上述代码中,定义了两个函数 rectangle_area()
和 rectangle_perimeter()
,这两个函数接收两个参数 width
和 height
,然后返回矩形的面积和周长。在主程序里,我们分别传入宽和高的值调用这两个函数来计算矩形的面积和周长。
面向对象的三大特性?分别解释下?
-
封装:把数据和操作数据的方法封装起来,对数据的访问只能通过已定义的接口。
-
继承:从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类,得到继承信息的被称为子类
-
多态:是指同一种消息可以被不同的对象以不同的方式处理。分为编译时多态(方法重载)和运行时多态(方法重写)。要实现多态需要做两件事:一是子类继承父类并重写父类中的方法,二是用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。
JDK
、JRE
、JVM
三者之间的关系?
-
JDK
:是 Java 开发工具包,是整个 Java 的核心,包括了 Java 运行环境JRE
、Java 工具和 Java 基础类库 -
JRE
:是 Java 的运行环境,包含JVM
标准实现及 Java 核心类库。 -
JVM
:是 Java 虚拟机,是整个 Java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。所有的 Java 程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。
重载和重写的区别?
-
重载是指在同一个类中,可以定义多个方法名相同但参数列表不同的方法(重载方法的参数列表必须不同)
-
重写是指在子类中重新定义父类中已经存在的方法。
-
发生的位置不同,重载发生在同一个类中,重写发生在子类中。
当谈论到重载和重写时,我们可以用具体的Java代码举例来说明它们的区别。
-
重载的示例:
public class Calculator { public int add(int a, int b) { return a + b; } public double add(double a, double b) { return a + b; } }
在上面的例子中,Calculator类中定义了两个名为add的方法。它们的方法名相同,但参数列表不同(一个是int类型,一个是double类型)。这就是重载的概念,通过参数列表的不同来区分方法。在调用add方法时,根据传入的参数类型会自动选择匹配的方法。
-
重写的示例:
public class Animal { public void makeSound() { System.out.println("Animal makes sound."); } } public class Cat extends Animal { @Override public void makeSound() { System.out.println("Cat meows."); } }
在上面的例子中,Animal类有一个名为makeSound的方法,Cat类继承自Animal类并重写了makeSound方法。在父类中,makeSound方法输出的是"Animal makes sound.",而在子类中,makeSound方法被重写,输出的是"Cat meows."。当使用多态的方式进行调用时,实际执行的是子类中重写的方法。
通过以上的例子,可以看到重载和重写的区别。重载是在同一个类中根据参数列表的不同定义多个方法,而重写是子类中重新定义父类中已经存在的方法。重载是通过参数来区分,而重写是通过方法的覆盖与多态来区分。
Java 中是否可以重写一个 private 或者 static 方法?
-
不能,重写是指在子类中重新定义父类中已经存在的方法
-
private方法只能在定义它的类内部访问,无法被子类继承和重写
-
static方法是属于类的方法,而不是实例的方法,也无法通过继承来进行重写
-
如果在子类中定义了与父类中的private或者static方法签名相同的方法,那么该方法只是在子类中新定义了一个方法,并不是重写父类的方法。
介绍Java中 private 关键字
当一个成员被声明为private时,它只能在同一个类内部被访问。以下是一些具体的Java代码示例来说明private关键字的使用。
-
访问私有字段:私有字段只能在类的内部访问,无法在其他类中直接访问。可以通过公共的方法或者访问器来间接地操作私有字段。
public class Person { private String name; // 私有字段 public String getName() { // 公共方法获取私有字段的值 return name; } public void setName(String name) { // 公共方法设置私有字段的值 this.name = name; } } public class Main { public static void main(String[] args) { Person person = new Person(); person.setName("Alice"); // 通过公共方法设置私有字段的值 System.out.println(person.getName()); // 通过公共方法获取私有字段的值 } }
-
私有方法:私有方法只能在类的内部被调用,无法在其他类中直接调用。
public class Calculator { private int add(int a, int b) { // 私有方法 return a + b; } public int calculateSum(int a, int b) { // 公共方法调用私有方法 return add(a, b); } } public class Main { public static void main(String[] args) { Calculator calculator = new Calculator(); int sum = calculator.calculateSum(2, 3); // 通过公共方法调用私有方法 System.out.println("Sum: " + sum); } }
在上述示例中,私有字段和私有方法都只能在声明它们的类内部访问或调用。通过提供公共的方法来间接操作私有成员,实现了数据隐藏和访问限制的目的。
通过使用private关键字,可以保护类的内部细节,提高代码的安全性、可控性和可维护性。
介绍Java中 static 关键字
在Java中,static是一个关键字,用于修饰类的成员(字段、方法和内部类),以及用于修饰代码块和内部接口。使用static关键字可以使成员与类本身相关联,而不是与类的实例对象相关联。
以下是对Java中static关键字的介绍:
-
静态字段(Static Fields):被声明为static的字段属于类级别,而不是实例级别。这意味着无论创建多少个类的实例对象,静态字段都只有一份拷贝。静态字段可以通过类名直接访问,无需实例对象。通常用于定义常量或者与类相关的全局数据。
示例:
public class MathUtils { public static final double PI = 3.14159; // 静态常量 public static int add(int a, int b) { // 静态方法 return a + b; } } public class Main { public static void main(String[] args) { System.out.println(MathUtils.PI); // 访问静态常量 int sum = MathUtils.add(2, 3); // 调用静态方法 System.out.println("Sum: " + sum); } }
-
静态方法(Static Methods):被声明为static的方法属于类级别,而不是实例级别。静态方法可以直接通过类名调用,无需实例对象。静态方法只能访问静态字段和调用其他静态方法,无法访问非静态字段和调用非静态方法(除非通过实例对象引用)。
示例:
public class MathUtils { public static int add(int a, int b) { // 静态方法 return a + b; } public static double calculateCircleArea(double radius) { // 静态方法 return PI * radius * radius; // 访问静态常量 } } public class Main { public static void main(String[] args) { int sum = MathUtils.add(2, 3); // 调用静态方法 System.out.println("Sum: " + sum); double area = MathUtils.calculateCircleArea(2.5); // 调用静态方法 System.out.println("Circle area: " + area); } }
-
静态代码块(Static Blocks):静态代码块是在类加载时执行,并且只会执行一次。它常用于初始化类的静态字段或执行其他静态操作。静态代码块按照声明的顺序执行。
示例:
public class Main { static { System.out.println("Static block 1"); } static { System.out.println("Static block 2"); } public static void main(String[] args) { System.out.println("Main method"); } }
输出结果:
Static block 1 Static block 2 Main method
-
静态内部类(Static Inner Classes):静态内部类是定义在另一个类中,并使用static修饰的内部类。静态内部类与外部类的实例对象无关,可以直接通过外部类名访问。与非静态内部类不同的是,静态内部类无法直接访问外部类的非静态字段和方法(除非通过实例对象引用)。
示例:
public class OuterClass { private static String message = "Hello, World!"; public static class StaticInnerClass { public void printMessage() { System.out.println(message); // 访问外部类的静态字段 } } } public class Main { public static void main(String[] args) { OuterClass.StaticInnerClass innerClass = new OuterClass.StaticInnerClass(); innerClass.printMessage(); // 调用静态内部类的方法 } }
上述是对Java中static关键字的基本介绍,它可以用于定义静态字段、静态方法、静态代码块和静态内部类。通过使用static关键字,可以将成员与类本身关联起来,提供类级别的访问和操作。
构造方法有哪些特性?
-
名字与类名相同;
-
没有返回值,但不能用 void 声明构造函数;
-
成类的对象时自动执行,无需调用。
构造方法(Constructor)是一种特殊的方法,用于创建对象并初始化对象的实例变量。在Java中,构造方法具有以下特性:
-
构造方法的名称必须与类名相同,且没有返回类型,也不用声明void类型。这意味着构造方法不能返回任何值,包括void。
-
在创建对象时,构造方法会自动被调用,用于初始化对象的实例变量。因为构造方法在创建对象时被调用,所以构造方法不能手动调用,也不能在类外部被调用。
-
如果没有定义任何构造方法,则编译器会自动生成一个默认构造方法,该构造方法不带参数,并且将实例变量都初始化为默认值(数值类型为0,布尔类型为false,对象引用为null)。
-
如果定义了至少一个构造方法,则编译器将不会自动生成默认构造方法。
-
如果一个类中有多个构造方法,它们可以通过重载来实现,即构造方法的参数列表不同,可以有不同的构造方法。
-
构造方法可以是公共的、私有的或受保护的。如果构造方法是私有的,则它只能在类内部被调用,因此可以用于限制类的实例化,也可用于单例模式的实现。
下面是一个简单的示例,演示了如何定义构造方法和创建对象:
public class Person { private String name; private int age; public Person(String name, int age) { // 构造方法 this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public static void main(String[] args) { Person person = new Person("Tom", 18); // 创建对象并初始化 System.out.println("Name: " + person.getName()); System.out.println("Age: " + person.getAge()); } }
在上面的示例中,Person类有一个构造方法,它接受两个参数name和age,并将它们分别赋值给实例变量。在main方法中,创建了一个Person对象,并将name和age分别设置为"Tom"和18。最后打印出对象的属性值。
在 Java 中定义一个不做事且没有参数的构造方法有什么作用?
-
默认初始化
-
反射调用
-
子类继承
Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法。
因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是:在父类里加上一个不做事且没有参数的构造方法。
在Java中定义一个不做事且没有参数的构造方法(无参构造方法)可以有以下作用:
-
默认初始化:如果没有显式定义任何构造方法,Java编译器会自动为类生成一个默认的无参构造方法,该构造方法会将实例变量初始化为默认值。通过定义一个自定义的无参构造方法,可以覆盖默认的构造方法,以确保对象在创建时具有特定的初始状态。
-
反射调用:当使用反射机制创建对象时,无参构造方法是必需的。反射机制可以在运行时获取类的信息,并动态地创建类的实例。在这种情况下,如果类中没有定义无参构造方法,则无法使用反射来实例化该类。
-
子类继承:子类继承父类时,会默认调用父类的无参构造方法。如果父类没有明确提供无参构造方法,那么子类必须使用super关键字显式调用父类的有参构造方法。因此,在定义一个带有参数的构造方法时,最好也提供一个无参构造方法,以便子类继承时能够顺利进行。
-
代码可读性:通过显式定义无参构造方法,可以提高代码的可读性和可维护性。即使方法体内没有代码,通过构造方法的存在,可以清晰地表达出对象的创建和初始化行为。
下面是一个示例,演示了在Java中定义一个没有参数且不做事的构造方法的用法:
public class Person { private String name; private int age; public Person() { // 无参构造方法,不做任何事情 } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public static void main(String[] args) { Person person = new Person(); // 调用无参构造方法创建对象 System.out.println("Name: " + person.getName()); // 输出为null System.out.println("Age: " + person.getAge()); // 输出为0 Person person2 = new Person("Tom", 18); // 调用有参构造方法创建对象 System.out.println("Name: " + person2.getName()); // 输出为"Tom" System.out.println("Age: " + person2.getAge()); // 输出为18 } }
在上面的示例中,Person类定义了一个无参构造方法,该方法不执行任何操作。在main方法中,通过调用无参构造方法创建了一个Person对象,并输出了其属性值。可以看到,由于无参构造方法没有对实例变量进行初始化,因此输出的值为默认值(null和0)。
问:但是类不是会有一个默认的无参构造吗
写一个带参的构造方法,默认的无参构造方法就没了,要自己加上去
Java 中创建对象的几种方式?
-
使用 new 关键字;
-
使用 Class 类的
newInstance
方法,该方法调用无参的构造器创建对象(反射):Class.forName.newInstance()
; -
使用 clone() 方法;
-
反序列化,比如调用
ObjectInputStream
类的readObject()
方法。
抽象类和接口有什么区别?
共同点 :
-
都不能被实例化。
-
都可以包含抽象方法。
-
都可以有默认实现的方法(Java 8 可以用 default 关键在接口中定义默认方法)。
区别:
-
接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系(比如说我们抽象了一个发送短信的抽象类,)。
-
一个类只能继承一个类,但是可以实现多个接口。
-
接口中(方法默认:
public abstrat
、成员变量默认:public static final
),成员变量不能被修改且必须有初始值。而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
静态变量和实例变量的区别?
-
静态变量:是被 static 修饰的变量,也称为类变量,它属于类,因此不管创建多少个对象,静态变量在Java内存中(方法区)有且仅有一个拷贝;静态变量可以实现让多个对象共享内存。
-
实例变量:属于某一实例,需要先创建对象,然后通过对象才能访问到它。
静态变量和实例变量是Java中两种不同类型的变量,它们在定义、内存分配和使用方式上有一些区别。
-
定义方式:静态变量使用static关键字在类级别进行定义,属于类的一部分;实例变量在类中定义,属于对象的一部分。
public class MyClass { static int staticVariable; // 静态变量 int instanceVariable; // 实例变量 }
-
内存分配:静态变量在程序启动时被分配内存空间,并且只有一份副本,在整个程序的生命周期内都存在;实例变量在每次创建对象时被分配内存空间,每个对象都有自己的实例变量。
-
使用方式:静态变量可以通过类名直接访问,无需创建对象;实例变量需要通过对象来访问。
MyClass.staticVariable = 10; // 访问静态变量 MyClass obj = new MyClass(); obj.instanceVariable = 20; // 访问实例变量
-
生命周期:静态变量的生命周期与整个程序的运行时间相同,即使没有对象存在,静态变量仍然存在;实例变量的生命周期与对象的创建和销毁过程相关,当对象销毁时,实例变量也会被销毁。
-
共享性:静态变量被所有该类的对象所共享,可以在任何对象中进行修改,一次修改会影响到所有对象;实例变量属于对象私有,每个对象拥有独立的实例变量副本,相互之间不会影响。
静态变量通常用于表示和类相关的属性,比如计数器、全局配置等;而实例变量通常用于表示每个对象自己的状态和数据。
需要注意的是,静态变量虽然可以在任何地方访问,但过多地使用静态变量可能导致代码可读性和维护性下降,因此应谨慎使用。另外,静态变量在多线程环境下需要考虑线程安全性。
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 编译器会对它进行特殊处理,因此可以正确编译。
在Java中,对于short类型的变量进行运算时会发生自动类型提升。因此,在short类型的变量进行算术运算或赋值运算时,会先将short类型转换为int类型进行运算,然后再将结果转回short类型。
-
对于表达式
s1 = s1 + 1
,由于s1
是short类型,s1 + 1
会先将s1
和1都转换为int类型进行相加,最后得到的结果是int类型,无法直接赋值给short类型的s1
,因此会编译错误。可以通过强制类型转换解决该问题:
short s1 = 1; s1 = (short) (s1 + 1);
-
对于表达式
s1 += 1
,这是一个复合赋值运算符,相当于s1 = s1 + 1
。但是,由于复合赋值运算符会隐式地进行类型转换,所以s1 += 1
是合法的,不会编译错误。在执行过程中,s1 + 1
会进行自动类型提升,但是由于赋值操作符的存在,最终的结果会强制转换回short类型,并赋值给s1
。
short s1 = 1; s1 += 1;
上述代码不会产生编译错误。
需要注意的是,在进行任何数值运算时,都要考虑数据溢出的问题。对于short类型,它的取值范围是-32768到32767。如果运算结果超出了这个范围,就会产生溢出错误。因此,在进行复杂运算时需要谨慎处理数据类型和溢出问题。
Integer 和 int 的区别?
-
int 是 Java 的八种基本数据类型之一,而 Integer 是 Java 为 int 类型提供的封装类;
-
int 型变量的默认值是 0,Integer 变量的默认值是 null,这一点说明 Integer 可以区分出未赋值和值为 0 的区分;
-
Integer 变量必须实例化后才可以使用,而 int 不需要。
装箱和拆箱的区别?
-
自动装箱是 Java 编译器在基本数据类型和对应得包装类之间做的一个转化。比如:把 int 转化成 Integer,double 转化成 Double 等等。反之就是自动拆箱。
-
原始类型:boolean、char、byte、short、int、long、float、double
-
封装类型:Boolean、Character、Byte、Short、Integer、Long、Float、Double
switch 语句能否作用在 byte 上,能否作用在 long 上,能否作用在 String 上?
在 switch(expr 1) 中,expr1 只能是一个整数表达式或者枚举常量。
-
由于,byte、short、char 都可以隐式转换为 int,
-
long:因为long的取值范围较大,在switch语句中进行比较时会发生编译错误。
-
JDK1.7 版本之后 switch 就可以作用在 String 上了。
在Java中,switch语句可以作用在byte、short、char和int这四种整数类型上。从Java SE7开始,还支持在String类型上使用switch语句。
具体情况如下:
-
byte:可以在byte类型上使用switch语句。由于byte是整数类型,可以作为switch表达式的取值范围。
示例代码:
byte num = 1; switch (num) { case 1: // 执行逻辑 break; case 2: // 执行逻辑 break; default: // 执行逻辑 break; }
-
long:不能在long类型上使用switch语句。因为long的取值范围较大,在switch语句中进行比较时会发生编译错误。
-
String:可以在String类型上使用switch语句。在Java SE7之后的版本中,Java引入了对字符串的switch支持。在switch表达式中使用字符串常量(不支持变量),并且可以在case分支中使用字符串常量进行判断。
示例代码:
String str = "hello"; switch (str) { case "hello": // 执行逻辑 break; case "world": // 执行逻辑 break; default: // 执行逻辑 break; }
需要注意的是,在使用String类型的switch语句时,case分支必须是字符串常量(包括字面值),而不是变量或表达式。在比较两个字符串时,使用的是equals()
方法而不是==
运算符。
总结起来,switch语句可以作用在byte类型和String类型上,但不能作用在long类型上。
final、finally、finalize 的区别?
-
final:用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、被其修饰的类不可继承;
-
finally:异常处理语句结构的一部分,表示总是执行;
-
finallize:Object类的一个方法,在垃圾回收时会调用被回收对象的finalize
== 和 equals 的区别?
-
= =:
-
如果比较的对象是基本数据类型,则比较的是数值是否相等;
-
如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
-
-
equals 方法:
-
用来比较两个对象的内容是否相等。
-
注意:equals 方法不能用于比较基本数据类型的变量。如果没有对 equals 方法进行重写,则比较的是引用类型的变量所指向的对象的地址(很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等)。
-
在Java中,==
和equals()
都是用于比较两个对象是否相等的方法,但是它们的比较方式是不同的,具体区别如下:
-
==
运算符
==
运算符用于判断两个变量或对象的引用是否指向同一个内存地址,即判断两个对象是否是同一个对象。如果两个对象的引用指向同一个内存地址,则==
运算符返回true;否则返回false。
示例代码:
String str1 = "hello"; String str2 = "hello"; String str3 = new String("hello"); System.out.println(str1 == str2); // true,因为str1和str2指向同一个字符串常量池中的对象 System.out.println(str1 == str3); // false,因为str1和str3指向不同的对象
需要注意的是,对于基本数据类型(int、float等),==
运算符比较的是变量的数值是否相等;而对于对象类型,则比较的是变量的引用是否相等。例如:
int a = 10; int b = 10; System.out.println(a == b); // true,因为a和b的值相等
-
equals()
方法
equals()
方法用于比较两个对象的内容是否相等,即属性是否相等。默认情况下,equals()
方法和==
运算符的作用相同,即用于比较两个对象的引用是否相等。但是,我们可以在类中重写equals()
方法,通过自定义实现来判断两个对象是否相等。
示例代码:
public class MyClass { private int id; private String name; // 构造函数、getter和setter省略 @Override public boolean equals(Object obj) { if (obj == null) return false; // 判断传入的对象是否为null if (!(obj instanceof MyClass)) return false; // 判断传入的对象是否为MyClass类型 if (this == obj) return true; // 判断是否同一个对象 MyClass other = (MyClass) obj; // 通过比较属性判断是否相等 if (this.id != other.getId()) return false; if (this.name == null && other.getName() != null) return false; if (this.name != null && other.getName() == null) return false; if (this.name != null && other.getName() != null && !this.name.equals(other.getName())) return false; return true; } } MyClass obj1 = new MyClass(1, "hello"); MyClass obj2 = new MyClass(1, "hello"); System.out.println(obj1.equals(obj2)); // true,因为两个对象的id和name属性都相等
需要注意的是,重写equals()
方法时应该满足以下要求:
-
自反性:对于任何非null的引用值x,x.equals(x)都应该返回true。
-
对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回true。
-
传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也应该返回true。
-
一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会始终返回true或者始终返回false。
-
对于任何非null的引用值x,x.equals(null)都应该返回false。
两个对象的 hashCode() 相同,则 equals() 也一定为 true 吗?
-
Object
类中的hashCode()
方法返回的是对象的哈希码,而equals()
方法则判断两个对象是否相等。在默认情况下,hashCode()
方法返回的是对象的内存地址对应的整数值。 -
如果两个对象的
hashCode()
相同,说明它们的哈希码相同,但并不能保证它们的内容一定相同,因此它们的equals()
方法也不一定相等。
好的,我来举个例子。
假设我们有一个Person
类,其定义如下:
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } }
默认情况下,Person
类没有重写equals()
和hashCode()
方法,因此它们都是从Object
类继承而来的。下面我们创建两个Person
对象,它们的内容相同但是它们是不同的对象。
Person p1 = new Person("张三", 20); Person p2 = new Person("张三", 20); System.out.println("p1.equals(p2): " + p1.equals(p2)); System.out.println("p1.hashCode(): " + p1.hashCode()); System.out.println("p2.hashCode(): " + p2.hashCode());
上述代码运行结果如下:
p1.equals(p2): false p1.hashCode(): 1787208305 p2.hashCode(): 1658926463
可以看到,尽管p1
和p2
的内容相同,但它们属于不同的对象,因此equals()
方法返回false
。另外,p1
和p2
的hashCode()
方法返回的值也不相同,这是因为它们在内存中的地址不同。
我们再来看下面的代码:
Person p1 = new Person("张三", 20); Person p2 = new Person("张三", 20); Map<Person, String> map = new HashMap<>(); map.put(p1, "A"); map.put(p2, "B"); System.out.println(map.get(p1)); // 输出:A System.out.println(map.get(p2)); // 输出:B
上述代码先创建了两个Person
对象p1
和p2
,然后将它们放入一个HashMap
中。由于p1
和p2
的内容相同,因此它们的哈希码也相同,而且重写hashCode()
和equals()
方法可以使它们相等。这时,我们通过map.get
方法分别获取p1
和p2
对应的值。由于它们的哈希码相同,并且equals()
方法也返回true
,因此它们被视为相同的key,最后输出的结果都是正确的。
综上所述,hashCode()
方法返回相等并不意味着equals()
方法一定相等,但如果同时重写hashCode()
和equals()
方法,则可以保证它们的语义一致。
哈希冲突(补充)
为什么重写 equals() 就一定要重写 hashCode() 方法?
-
Java 官方建议重写 equals() 的时候也重写 hashCode() 方法。
-
重写 hashCode()可以减少了 equals() 比较次数
这个问题应该是有个前提,就是你需要用到 HashMap、HashSet 等 Java 集合,用不到哈希表的话,其实仅仅重写 equals() 方法也可以。而工作中的场景是常常用到 Java 集合,所以 Java 官方建议重写 equals() 就一定要重写 hashCode() 方法。
对于对象集合的判重,如果一个集合含有 10000 个对象实例,仅仅使用 equals() 方法的话,那么对于一个对象判重就需要比较 10000 次,随着集合规模的增大,时间开销是很大的。但是同时使用哈希表的话,就能快速定位到对象的大概存储位置,并且在定位到大概存储位置后,后续比较过程中,如果两个对象的 hashCode 不相同,也不再需要调用 equals() 方法,从而大大减少了 equals() 比较次数。
所以从程序实现原理上来讲的话,既需要 equals() 方法,也需要 hashCode() 方法。那么既然重写了 equals(),那么也要重写 hashCode() 方法,以保证两者之间的配合关系。
hashCode()与equals()的相关规定:
1、如果两个对象相等,则 hashCode 一定也是相同的;
2、两个对象相等,对两个对象分别调用 equals 方法都返回 true;
3、两个对象有相同的 hashCode 值,它们也不一定是相等的;
4、因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖;
5、hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
& 和 && 的区别?
-
&& 和 & 都是表示与的逻辑运算符,
-
&&:有短路功能,当第一个表达式的值为 false 的时候,则不再计算第二个表达式;
-
&:不管第一个表达式结果是否为 true,第二个都会执行。除此之外,& 还可以用作位运算符:当 & 两边的表达式不是 Boolean 类型的时候,& 表示按位操作。
Java 中的参数传递,是值传递呢?还是引用传递?
-
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
-
java中只有值传递。
在 Java 中,参数传递分为两种方式:值传递和引用传递。
-
值传递(Pass-by-Value):在值传递中,方法得到的是原始数据的一个副本,也就是说,在方法内部对参数进行修改并不会影响到原始数据。
-
引用传递(Pass-by-Reference):在引用传递中,方法得到的是原始数据的引用,也就是说,可以通过这个引用在方法内部直接修改原始数据。
然而,在 Java 中,无论参数是基本数据类型还是对象类型,实际上都是采用了值传递的方式进行参数传递,这就是上述说法中的关键。
-
对于基本数据类型(如
int
、boolean
等),当我们将其作为参数传递给方法时,实际上是将原始值的一个副本传递给方法。因此,方法内部对参数的修改并不会影响到原始变量。 -
对于对象类型,当我们将对象作为参数传递给方法时,实际上是传递了对象的引用值(内存地址)的一个副本。因此,在方法内部可以通过这个引用值直接修改对象的属性,或者通过方法调用修改对象的内部状态。但是,如果方法内部为该参数重新赋予一个新的对象,那么原始变量的引用不会受到影响。
以下示例代码展示了 Java 中参数传递的特点:
javaCopy Codepublic class ParameterPassingExample { public static void main(String[] args) { int a = 10; int b = 20; swap(a, b); System.out.println("a = " + a); // 输出 a = 10 System.out.println("b = " + b); // 输出 b = 20 int[] arr = {1, 2, 3}; changeArray(arr); System.out.println(Arrays.toString(arr)); // 输出 [1, 2, 3, 4, 5] String str = "Hello"; changeString(str); System.out.println(str); // 输出 Hello } // 交换两个整数的值 public static void swap(int x, int y) { int temp = x; x = y; y = temp; } // 向数组中添加两个元素 public static void changeArray(int[] array) { array = Arrays.copyOf(array, array.length + 2); array[array.length - 2] = 4; array[array.length - 1] = 5; } // 将字符串转为全大写 public static void changeString(String s) { s = s.toUpperCase(); } }
在上述代码中,swap
方法交换了两个整数的值但是并没有影响到原始变量,changeArray
方法向数组中添加了两个元素,并且这些修改可以反映到原始对象中。而changeString
方法尝试将字符串转为全大写,但是并没有成功,因为方法中重新赋值并不会影响原始变量的引用。
因此,我们可以得出结论,Java 中的参数传递是按值传递,但如果参数是一个对象,那么其实传递的是对象的引用。
Java 中的 Math.round(-1.5) 等于多少?
-
等于 -1,因为在数轴上取值时,中间值(0.5)向右取整,所以正 0.5 是往上取整,负 0.5 是直接舍弃。
Math 是 Java 提供的一个数学工具类,下面我将详细介绍一些 Math 类中常用的方法。
-
基本数学运算方法:
-
abs(x)
:返回参数 x 的绝对值。 -
max(x, y)
:返回 x 和 y 中的较大值。 -
min(x, y)
:返回 x 和 y 中的较小值。 -
sqrt(x)
:返回参数 x 的平方根。 -
cbrt(x)
:返回参数 x 的立方根。 -
pow(x, y)
:返回 x 的 y 次幂(x^y)的值。 -
exp(x)
:返回 e (自然对数的底数)的 x 次幂。 -
log(x)
:返回参数 x 的自然对数(以 e 为底)。 -
log10(x)
:返回参数 x 的以 10 为底的对数。
-
三角函数:
-
sin(x)
:计算参数 x 的正弦值。 -
cos(x)
:计算参数 x 的余弦值。 -
tan(x)
:计算参数 x 的正切值。 -
asin(x)
:计算参数 x 的反正弦值。 -
acos(x)
:计算参数 x 的反余弦值。 -
atan(x)
:计算参数 x 的反正切值。
-
取整和舍入:
-
ceil(x)
:向上取整,返回大于或等于参数 x 的最小整数。 -
floor(x)
:向下取整,返回小于或等于参数 x 的最大整数。 -
round(x)
:四舍五入,返回参数 x 最接近的整数。 -
rint(x)
:返回与参数 x 最接近的整数,如果有两个整数同样接近,则返回偶数。
-
随机数生成:
-
random()
:返回一个介于 0.0 和 1.0 之间的随机双精度浮点数。
-
其他常用方法:
-
toDegrees(x)
:将参数 x 从弧度转换为角度。 -
toRadians(x)
:将参数 x 从角度转换为弧度。 -
signum(x)
:返回参数 x 的符号函数:-1.0(负数),0.0(零),或 1.0(正数)。
Math 类中的方法都是静态方法,可以直接通过类名调用,例如 Math.abs(-5)
返回 5,Math.sqrt(16)
返回 4.0。
需要注意的是,由于 Math 类的方法都是基于 IEEE 754 浮点算术标准实现的,所以在使用这些方法时可能会有一些舍入误差。
Math 中的random()
Math.random()
方法返回一个介于 0.0 和 1.0 之间(包括 0.0,但不包括 1.0)的 double 值。可以将其乘以最大值,来得到任意范围内的随机数。
以下是生成一个介于1和100之间的随机整数的代码示例:
int randomNum = (int) (Math.random() * 100) + 1;
这段代码首先调用 Math 类的 random()
方法,生成一个介于 0.0 和 1.0 之间的 double 值。然后将其乘以 100,得到一个介于 0.0 和 100.0 之间的 double 值。最后,将得到的 double 值转换为 int 类型,并加上 1,以确保随机数在 1 到 100 之间。
请注意,在使用 Math.random()
生成随机数时,您需要将生成的 double 值转换为所需的数据类型,并根据需要进行适当的缩放。此外,由于 Math.random()
使用伪随机数生成器实现,因此在每次运行程序时都会生成相同的序列。若要生成真正随机的序列,请考虑使用 Random 类或 SecureRandom 类。
Random中的random()
java.util.Random
是 Java 中的一个类,用于生成伪随机数序列。Random 类提供了多种方法来生成不同范围、不同分布和不同类型的随机数。
以下是一些 Random 类的常见方法:
-
nextInt(int n)
:返回一个介于 0(包括)和 n(不包括)之间的随机整数。 -
nextDouble()
:返回一个介于 0.0 和 1.0 之间的随机双精度浮点数。 -
nextBoolean()
:返回一个随机的布尔值。 -
nextBytes(byte[] bytes)
:生成随机字节序列,并将其存储到指定的 byte 数组中。 -
nextGaussian()
:返回一个符合高斯分布的随机双精度浮点数。
可以创建 Random 对象,并使用其相应的方法来生成随机数。如果创建 Random 对象时没有提供种子值,则使用当前时间作为默认种子。如果您需要在多个地方使用相同的随机数序列,则可以传递相同的种子值来构造 Random 对象。
以下是一个简单示例,展示如何使用 Random 类生成一个介于 1 和 100 之间的随机整数:
import java.util.Random; public class RandomExample { public static void main(String[] args) { Random random = new Random(); int randomNumber = random.nextInt(100) + 1; System.out.println("随机数:" + randomNumber); } }
Random 对象的 nextInt(int n)
方法将返回一个介于 0(包括)和 100(不包括)之间的随机整数。为了将范围扩展到 1 到 100,我们在结果上加 1。最后,通过打印语句输出生成的随机数。
请注意,每次运行该程序时,都会生成一个不同的随机数。
如何实现对象的克隆?
-
实现 Cloneable 接口并重写 Object 类中的 clone() 方法;
-
实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。
对象克隆是在 Java 中将一个对象的所有字段值复制到另一个新对象中的过程。因为 Java 是面向对象编程语言,所以对象是程序的基本构建块之一。有时我们需要创建一个与原始对象具有相同状态的新对象,这就是对象克隆的作用。
在 Java 中,可以使用 java.lang.Cloneable
接口和 Object
类的 clone()
方法来实现对象克隆。要确保一个类可以被克隆,该类必须实现 Cloneable
接口并重写 Object
类的 clone()
方法。默认情况下,clone()
方法会创建一个浅克隆,即复制对象的所有字段值并返回一个新对象。但是,如果对象包含对其他对象的引用,则复制的仅仅是引用而不是引用指向的对象。这时,可以通过重写 clone()
方法并深度克隆每个对象来实现深克隆。
深克隆(Serializable
)是指在复制对象时,不仅复制对象本身的字段值,还要递归复制其关联对象的字段值。除了使用重写 clone()
方法实现深克隆外,还可以通过对象的序列化和反序列化来实现真正的深克隆。
具体步骤如下:
-
实现
Serializable
接口:需要确保被克隆的类及其关联的所有类都实现了Serializable
接口。 -
将对象序列化:使用
ObjectOutputStream
将对象写入字节流,并保存到临时文件或内存中。 -
从字节流中反序列化对象:使用
ObjectInputStream
从字节流中读取对象,并返回一个新对象。
下面是一个示例代码,展示如何通过序列化和反序列化实现深克隆:
import java.io.*; public class Person implements Serializable { private String name; private int age; private Address address; // 构造方法... // 深克隆方法 public Person deepClone() throws IOException, ClassNotFoundException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(this); // 序列化对象 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); return (Person) ois.readObject(); // 反序列化并返回新对象 } // 内部类 Address... }
在此示例中,Person
类实现了 Serializable
接口,并提供了 deepClone()
方法。该方法使用两个临时字节流 ByteArrayOutputStream
和 ByteArrayInputStream
来进行序列化和反序列化操作,从而实现深克隆。
需要注意的是,被克隆的类及其关联的所有类都必须实现 Serializable
接口,否则会抛出 NotSerializableException
异常。
通过序列化和反序列化实现的深克隆可以复制对象及其关联对象的状态,因为对象在序列化和反序列化过程中会被写入和读取到一个新的字节序列中。但是,这种方式的效率相对较低,而且需要确保对象及其关联对象都可以序列化。
因此,在实现深克隆时,可以根据具体需求选择使用重写 clone()
方法还是序列化和反序列化方式来实现。
深克隆和浅克隆的区别?
-
浅克隆:拷贝对象和原始对象的引用类型引用同一个对象。浅克隆只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化
-
深克隆:拷贝对象和原始对象的引用类型引用不同对象。深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝
深克隆和浅克隆都是对象克隆的两种方式,它们之间的区别在于复制对象时是否复制其关联的其他对象。具体来说,浅克隆创建一个新对象并将原始对象的字段值复制到新对象中,但它并不复制原始对象引用的其他对象。而深克隆会递归地复制所有关联的对象,以便整个对象图都被复制。
以下是一个示例,展示如何实现对象的深克隆和浅克隆:
public class Person implements Cloneable { private String name; private int age; private Address address; // 包含对其他对象的引用 // 构造方法... // 浅克隆方法 public Person clone() throws CloneNotSupportedException { return (Person) super.clone(); } // 深克隆方法 public Person deepClone() throws CloneNotSupportedException { Person clone = (Person) super.clone(); clone.address = this.address.clone(); // 递归地克隆 Address 对象 return clone; } // 其他方法... // 内部类 Address private static class Address implements Cloneable { private String street; private String city; // 构造方法... // 克隆方法 public Address clone() throws CloneNotSupportedException { return (Address) super.clone(); } // 其他方法... } }
在此示例中,Person
类包含一个对 Address
对象的引用。要实现深克隆,需要在 Person
类中重写 clone()
方法,并在其中递归地调用 clone()
方法来复制 Address
对象。相比之下,浅克隆的实现非常简单,只需要调用父类的 clone()
方法即可。
需要注意的是,在实现深克隆时,不仅需要确保对象本身可以被克隆,而且所引用的对象也都需要支持克隆操作。在此示例中,Address
类也实现了 Cloneable
接口并重写了 clone()
方法。
另外,需要注意的是,使用对象克隆可能会带来一些副作用,例如,如果克隆后的对象包含对 static
变量的引用,则这些变量将会指向原始对象的值,而不是克隆后的值。因此,在使用对象克隆时需要小心谨慎,确保对象关系清晰、正确。
补充:
深克隆的实现就是在引用类型所在的类实现 Cloneable 接口,并使用 public 访问修饰符重写 clone 方法。
Java 中定义的 clone 没有深浅之分,都是统一的调用 Object 的 clone 方法。为什么会有深克隆的概念?是由于我们在实现的过程中刻意的嵌套了 clone 方法的调用。也就是说深克隆就是在需要克隆的对象类型的类中重新实现克隆方法 clone()。
什么是 Java 的序列化,如何实现 Java 的序列化?
-
Java的序列化是一种将对象转换为字节流的过程
-
条件:
-
类必须实现
java.io.Serializable
接口 -
所有非静态和非瞬态变量(即不包含
transient
关键字修饰的变量)都将被序列化
-
Java的序列化是一种将对象转换为字节流的过程,可以将对象保存到文件中或通过网络传输。序列化后的字节流可以被反序列化为原始对象。
要实现Java的序列化,需要满足以下条件:
-
类必须实现
java.io.Serializable
接口。 -
所有非静态和非瞬态变量(即不包含
transient
关键字修饰的变量)都将被序列化。
下面是一个简单的Java代码示例:
import java.io.*; // 实现Serializable接口 class Person implements Serializable { private static final long serialVersionUID = 1L; private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public void display() { System.out.println("Name: " + name + ", Age: " + age); } } public class SerializationExample { public static void main(String[] args) { Person person = new Person("Alice", 25); try { // 序列化对象到文件 FileOutputStream fileOut = new FileOutputStream("person.ser"); ObjectOutputStream objOut = new ObjectOutputStream(fileOut); objOut.writeObject(person); objOut.close(); fileOut.close(); System.out.println("Person对象已序列化到文件person.ser"); } catch (IOException e) { e.printStackTrace(); } // 从文件反序列化对象 try { FileInputStream fileIn = new FileInputStream("person.ser"); ObjectInputStream objIn = new ObjectInputStream(fileIn); Person serializedPerson = (Person) objIn.readObject(); objIn.close(); fileIn.close(); System.out.println("从文件person.ser反序列化Person对象:"); serializedPerson.display(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } }
在上面的示例中,我们创建了一个Person
类,实现了Serializable
接口。然后我们将一个Person
对象序列化到文件中,并从文件中反序列化为一个新的Person
对象。
什么情况下需要序列化?
-
对象需要在网络上传输
-
对象需要永久保存
-
对象需要跨应用程序或平台共享
-
通过RMI实现远程方法调用
在Java中,需要将对象序列化的情况通常包括如下几种:
-
对象需要在网络上传输。在分布式系统中,如果需要将对象从一个计算机传输到另一个计算机,就需要将对象序列化为字节流。
-
对象需要永久保存。如果需要将一个对象保存到磁盘或者数据库中,或者需要进行对象的备份,也需要将对象序列化。
-
对象需要跨应用程序或平台共享。如果需要将一个对象传递给其他应用程序或操作系统,需要序列化为跨平台的格式。
-
通过RMI实现远程方法调用。Java的远程方法调用(RMI)需要将参数和返回值序列化以便在网络上传输。
总之,当需要将一个对象在不同的环境中传输或保存时,就需要将它序列化。
Java 的泛型是如何工作的 ? 什么是类型擦除 ?
-
泛型是一种在编译时期进行类型检查的机制
-
类型擦除是指在生成的字节码中去除泛型类型信息,将泛型类型参数擦除为其上界或Object类型
一个被举了无数次的例子:
List arrayList = new ArrayList(); arrayList.add("aaaa"); arrayList.add(100); for(int i = 0; i< arrayList.size();i++){ String item = (String)arrayList.get(i); Log.d("泛型测试","item = " + item); }
毫无疑问,程序的运行结果会以崩溃结束:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList
可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
List<String> arrayList = new ArrayList<String>(); ... //arrayList.add(100); 在编译阶段,编译器就会报错
泛型只在编译阶段有效。看下面的代码:
List<String> stringArrayList = new ArrayList<String>(); List<Integer> integerArrayList = new ArrayList<Integer>(); Class classStringArrayList = stringArrayList.getClass(); Class classIntegerArrayList = integerArrayList.getClass(); if(classStringArrayList.equals(classIntegerArrayList)){ Log.d("泛型测试","类型相同"); }
输出结果:D/泛型测试: 类型相同
。
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
什么是泛型中的限定通配符和非限定通配符 ?
-
非限定通配符(没有指定上边界或下边界的通配符)
-
它用
?
来表示。比如List<?>
就是一个非限定通配符
-
-
限定通配符(对泛型变量的边界约束)
-
<? extends T>
:表示通配符可接受T及其子类类型的参数 -
<? super T>
:表示通配符可接受T及其父类类型的参数
-
List< ? extends T > 和 List < ? super T > 之间有什么区别 ?
-
一个只接受子类,一个只接受父类
Java 中的反射是什么意思?有哪些应用场景?
-
JAVA机制反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
-
框架开发、动态代理、单元测试、注解处理器、调试工具
Java中的反射(Reflection)是指在运行时动态地获取、检查和操作类、对象、方法和属性的能力。它允许程序在运行时获取类的信息并操作类或对象,而不需要在编译时确定。
反射机制提供了以下几个主要的功能:
-
动态获取类的信息:通过反射可以获取类的构造函数、方法、字段等信息,包括访问修饰符、参数类型、返回值类型等。
-
动态创建对象:通过反射可以实例化一个类,并调用其构造函数来创建对象。
-
动态调用方法:通过反射可以调用类的方法,包括公有方法、私有方法以及静态方法。
-
动态访问或修改属性:通过反射可以获取和设置类的字段,即使是私有字段。
-
注解处理:通过反射可以获取并处理类、方法、字段上的注解。
反射在许多应用场景中都很有用,例如:
-
框架开发:很多Java框架(如Spring、Hibernate等)使用反射来加载配置文件、实例化对象、调用方法等,以实现灵活的扩展和配置。
-
动态代理:反射可用于生成动态代理对象,实现AOP(面向切面编程)和远程方法调用等功能。
-
单元测试:使用反射可以轻松地访问私有方法和字段,方便编写单元测试,提高代码覆盖率。
-
注解处理器:Java编译器在处理注解时使用反射来获取注解信息,并根据注解生成相应的代码。
-
调试工具:反射提供了一种动态查看和操作类的能力,使调试工具如IDE、调试器等更加强大。
需要注意的是,反射机制虽然强大,但由于其涉及到运行时的类型检查以及额外的开销,使用反射可能会带来性能损失并增加代码的复杂性。因此,应谨慎使用反射,并优先考虑其他方式来解决问题。
反射的优缺点?
优点:
运行期类型的判断,class.forName() 动态加载类,提高代码的灵活度;
缺点:
(1)性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
(2)安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
(3)内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如:访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
Java 中的动态代理是什么?有哪些应用?
-
当想要给实现了某个接口的类中的方法,加一些额外的处理。如唱、调,rap,篮球,需要布置场地,和收钱,鸡哥只想ctrl.
-
Spring 的 AOP 、加事务、加权限、加日志。
怎么实现动态代理?
-
Java 中,实现动态代理有两种方式:
-
JDK 动态代理:java.lang.reflect 包中的 Proxy 类和 InvocationHandler 接口提供了生成动态代理类的能力。
-
Cglib 动态代理:Cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。
-
-
JDK 动态代理和 Cglib 动态代理的区别:
-
JDK 的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口。
-
cglib 代理的对象则无需实现接口,达到代理类无侵入。(如果想代理没有实现接口的类,就可以使用 CGLIB实现。)
-
动态代理三要素:
1,真正干活的对象
2,代理对象
3,利用代理调用方法
切记一点:代理可以增强或者拦截的方法都在接口中,接口需要写在newProxyInstance的第二个参数里。
public class Test { public static void main(String[] args) { /* 需求: 外面的人想要大明星唱一首歌 1. 获取代理的对象 代理对象 = ProxyUtil.createProxy(大明星的对象); 2. 再调用代理的唱歌方法 代理对象.唱歌的方法("只因你太美"); */ //1. 获取代理的对象 BigStar bigStar = new BigStar("鸡哥"); Star proxy = ProxyUtil.createProxy(bigStar); //2. 调用唱歌的方法 String result = proxy.sing("只因你太美"); System.out.println(result); } }
/* * * 类的作用: * 创建一个代理 * * */ public class ProxyUtil { /* * * 方法的作用: * 给一个明星的对象,创建一个代理 * * 形参: * 被代理的明星对象 * * 返回值: * 给明星创建的代理 * * * * 需求: * 外面的人想要大明星唱一首歌 * 1. 获取代理的对象 * 代理对象 = ProxyUtil.createProxy(大明星的对象); * 2. 再调用代理的唱歌方法 * 代理对象.唱歌的方法("只因你太美"); * */ public static Star createProxy(BigStar bigStar){ /* java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法: public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 参数一:用于指定用哪个类加载器,去加载生成的代理类 参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法 参数三:用来指定生成的代理对象要干什么事情*/ Star star = (Star) Proxy.newProxyInstance( ProxyUtil.class.getClassLoader(),//参数一:用于指定用哪个类加载器,去加载生成的代理类 new Class[]{Star.class},//参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法 //参数三:用来指定生成的代理对象要干什么事情 new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /* * 参数一:代理的对象 * 参数二:要运行的方法 sing * 参数三:调用sing方法时,传递的实参 * */ if("sing".equals(method.getName())){ System.out.println("准备话筒,收钱"); }else if("dance".equals(method.getName())){ System.out.println("准备场地,收钱"); } //去找大明星开始唱歌或者跳舞 //代码的表现形式:调用大明星里面唱歌或者跳舞的方法 return method.invoke(bigStar,args); } } ); return star; } }
public interface Star { //我们可以把所有想要被代理的方法定义在接口当中 //唱歌 public abstract String sing(String name); //跳舞 public abstract void dance(); }
public class BigStar implements Star { private String name; public BigStar() { } public BigStar(String name) { this.name = name; } //唱歌 @Override public String sing(String name){ System.out.println(this.name + "正在唱" + name); return "谢谢"; } //跳舞 @Override public void dance(){ System.out.println(this.name + "正在跳舞"); } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } public String toString() { return "BigStar{name = " + name + "}"; } }
static 关键字的作用?
-
static变量
java
中可以通过static
关键字修饰变量达到全局变量的效果。static修饰的变量(静态变量)属于类,在类第一次通过类加载器到jvm
时被分配内存空间。
-
static方法
static修饰的方法属于类方法,不需要创建对象就可以调用。static方法中不能使用this和super等关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态方法。
-
static 代码块
JVM
在加载类时会执行static代码块,static代码块常用于初始化静态变量,static代码只会在类被加载时执行且执行一次。
-
static内部类
static内部类可以不依赖外部类实例对象而被实例化,而内部类需要在外部类实例化后才能被实例化。静态内部类不能访问外部类的普通变量,只能访问外部类的静态成员变量和静态方法。
在Java中,static是一个关键字,用于修饰类的成员(字段、方法和内部类),以及用于修饰代码块和内部接口。使用static关键字可以使成员与类本身相关联,而不是与类的实例对象相关联。
以下是对Java中static关键字的介绍:
-
静态字段(Static Fields):被声明为static的字段属于类级别,而不是实例级别。这意味着无论创建多少个类的实例对象,静态字段都只有一份拷贝。静态字段可以通过类名直接访问,无需实例对象。通常用于定义常量或者与类相关的全局数据。
示例:
public class MathUtils { public static final double PI = 3.14159; // 静态常量 public static int add(int a, int b) { // 静态方法 return a + b; } } public class Main { public static void main(String[] args) { System.out.println(MathUtils.PI); // 访问静态常量 int sum = MathUtils.add(2, 3); // 调用静态方法 System.out.println("Sum: " + sum); } }
-
静态方法(Static Methods):被声明为static的方法属于类级别,而不是实例级别。静态方法可以直接通过类名调用,无需实例对象。静态方法只能访问静态字段和调用其他静态方法,无法访问非静态字段和调用非静态方法(除非通过实例对象引用)。
示例:
public class MathUtils { public static int add(int a, int b) { // 静态方法 return a + b; } public static double calculateCircleArea(double radius) { // 静态方法 return PI * radius * radius; // 访问静态常量 } } public class Main { public static void main(String[] args) { int sum = MathUtils.add(2, 3); // 调用静态方法 System.out.println("Sum: " + sum); double area = MathUtils.calculateCircleArea(2.5); // 调用静态方法 System.out.println("Circle area: " + area); } }
-
静态代码块(Static Blocks):静态代码块是在类加载时执行,并且只会执行一次。它常用于初始化类的静态字段或执行其他静态操作。静态代码块按照声明的顺序执行。
示例:
public class Main { static { System.out.println("Static block 1"); } static { System.out.println("Static block 2"); } public static void main(String[] args) { System.out.println("Main method"); } }
输出结果:
Static block 1 Static block 2 Main method
-
静态内部类(Static Inner Classes):静态内部类是定义在另一个类中,并使用static修饰的内部类。静态内部类与外部类的实例对象无关,可以直接通过外部类名访问。与非静态内部类不同的是,静态内部类无法直接访问外部类的非静态字段和方法(除非通过实例对象引用)。
示例:
public class OuterClass { private static String message = "Hello, World!"; public static class StaticInnerClass { public void printMessage() { System.out.println(message); // 访问外部类的静态字段 } } } public class Main { public static void main(String[] args) { OuterClass.StaticInnerClass innerClass = new OuterClass.StaticInnerClass(); innerClass.printMessage(); // 调用静态内部类的方法 } }
上述是对Java中static关键字的基本介绍,它可以用于定义静态字段、静态方法、静态代码块和静态内部类。通过使用static关键字,可以将成员与类本身关联起来,提供类级别的访问和操作。
super 关键字的作用?
-
调用父类的构造方法
-
访问父类的成员变量和方法
-
在子类中调用父类的方法
在Java中,super关键字有以下几个作用:
-
调用父类的构造方法:在子类的构造方法中使用super关键字可以调用父类的构造方法。通过super关键字可以显式地指定要调用的父类构造方法,并且可以传递参数。
-
访问父类的成员变量和方法:通过super关键字可以访问父类中被子类隐藏的成员变量和方法。当子类和父类有同名的成员变量或方法时,使用super关键字可以明确指定要访问的是父类中的成员。
-
在子类中调用父类的方法:子类可以通过super关键字调用父类中的非私有方法。这在子类需要扩展父类的功能时非常有用,可以先调用父类的方法来执行父类的逻辑,然后再在子类中添加额外的操作。
需要注意的是,super关键字只能在子类中使用,并且只能用于访问父类的构造方法、成员变量和非私有方法。
字节和字符的区别?
-
字节是存储容量的基本单位;
-
字符是数字、字母、汉字以及其他语言的各种符号;
-
1 字节 = 8 个二进制单位 即
1B(byte,字节)= 8 bit(位)
; -
一个字符由一个字节或多个字节的二进制单位组成;
String 为什么要设计为不可变类?
-
字符串常量池的需要:字符串常量池是 Java 堆内存中一个特殊的存储区域, 当创建一个 String 对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;
-
允许 String 对象缓存 HashCode:Java 中 String 对象的哈希码被频繁地使用, 比如在 HashMap 等容器中。字符串不变性保证了 hash 码的唯一性,因此可以放心地进行缓存。这也是一种性能优化手段,意味着不必每次都去计算新的哈希码;
-
String 被许多的 Java 类(库)用来当做参数,例如:网络连接地址 URL、文件路径 path、还有反射机制所需要的 String 参数等, 假若 String 不是固定不变的,将会引起各种安全隐患。
详细介绍String类
String类是Java中的一个核心类,用于表示和操作字符串。在Java中,字符串是以对象的形式来处理的,而String类提供了一系列方法来进行字符串的创建、比较、连接、截取等操作。
以下是String类的一些主要特性和常用方法:
-
不可变性:String对象一旦创建,其内容就不可改变。这意味着对字符串的任何改变都会创建一个新的String对象,而原始对象不会被修改。
-
字符串创建:可以使用字面量方式创建字符串,例如:
String str = "Hello World";
;也可以使用构造方法创建字符串,例如:String str = new String("Hello World");
。 -
字符串操作:String类提供了许多方法来操作字符串,例如:
-
获取字符串长度:
int length()
-
字符串拼接:
String concat(String str)
-
字符串替换:
String replace(CharSequence target, CharSequence replacement)
-
字符串分割:
String[] split(String regex)
-
字符串截取:
String substring(int beginIndex)
、String substring(int beginIndex, int endIndex)
-
字符串转换:
char[] toCharArray()
、byte[] getBytes()
-
字符串比较:
boolean equals(Object obj)
、int compareTo(String anotherString)
-
等等
-
-
字符串连接:在字符串连接操作中,Java提供了简化的语法糖,即使用加号(+)进行字符串连接。例如:
String str = "Hello" + "World";
需要注意的是,虽然String对象不可变,但是可以通过StringBuilder和StringBuffer类进行可变的字符串操作。这两个类提供了更高效的字符串拼接和修改方法。
由于String类广泛使用,Java编译器对其进行了优化,使得字符串操作更加高效。在实际开发中,建议对字符串频繁操作时,尽量使用StringBuilder或StringBuffer来提升性能。
StringBuilder和StringBuffer类
StringBuilder类和StringBuffer类是Java中的可变字符串类,可以在字符串中插入、删除、替换、修改、追加等操作。这两个类的主要区别在于线程安全性和性能。StringBuffer是线程安全的,而StringBuilder没有实现线程安全的功能,但性能更高。
以下是两个类的一些主要特性和常用方法:
-
StringBuilder类
-
StringBuilder类是一个非线程安全的、可变的字符序列。
-
StringBuilder类的主要方法有:append()、insert()、replace()、delete()等。
示例代码:
StringBuilder sb = new StringBuilder(); sb.append("Hello "); sb.append("World"); String str = sb.toString(); // "Hello World"
-
StringBuffer类
-
StringBuffer类是一个线程安全的、可变的字符序列。
-
StringBuffer类的主要方法与StringBuilder类相同,包括:append()、insert()、replace()、delete()等。
示例代码:
StringBuffer sb = new StringBuffer(); sb.append("Hello "); sb.append("World"); String str = sb.toString(); // "Hello World"
需要注意的是,由于StringBuilder和StringBuffer都是可变字符串类,因此它们不需要频繁创建新的对象,而能够重复利用已有的内存空间,从而提高代码的执行效率。但如果多个线程同时对同一对象进行操作时,使用StringBuilder可能会因为线程安全问题而引发异常,此时建议使用StringBuffer类。
总之,StringBuilder和StringBuffer类都是非常实用的字符串操作类,可以提高Java代码的性能和灵活性。
String、StringBuilder、StringBuffer 的区别?
-
String:用于字符串操作,属于不可变类
-
StringBuilder:用于字符串操作,可变类,线程不安全
-
StringBuffer:用于字符串操作,可变类,对方法加了同步锁,线程安全
-
执行效率:StringBuilder > StringBuffer > String
扩展
在1.8及以前,String类底层使用的是char[],1.9之后改成了byte[]
因为字符串较多是拉丁字母,只需要一个字节就可以存储,char多出来的一个字节容易浪费,gc更频繁
String不可变是因为char[]数组被final修饰了,而StringBuilder和StringBuffer没有(继承了AbstractStringBuilder中的数组)
为了实现线程同步,需要牺牲一部分的效率,所以StringBuilder的效率要比StringBuffer高
String 字符串修改实现的原理?
-
String被设计为不可变类,即一旦创建就无法修改其值。当对String进行拼接、替换等操作时,并不会直接修改原有的String对象,而是通过创建新的String对象来表示修改后的字符串。
-
当用 String 类型来对字符串进行修改时,其实现方法是首先创建一个 StringBuilder,其次调用 StringBuilder 的 append() 方法,最后调用 StringBuilder 的 toString() 方法把结果返回。用源码解读
确切地说,当使用 String 类型对字符串进行修改时,并不一定会始终创建 StringBuilder 对象。这取决于具体的操作和编译器的优化。
在某些情况下,编译器会自动将字符串连接操作转换为使用 StringBuilder 的方式来实现。例如,下面的代码片段:
javaCopy CodeString str = "Hello"; str += " World";
实际上会被编译器优化为以下代码:
javaCopy CodeString str = new StringBuilder("Hello").append(" World").toString();
可以看到,编译器先创建了一个 StringBuilder 对象并将初始字符串 "Hello" 传递给它,然后调用了 append() 方法来追加新的字符串 " World",最后通过 toString() 方法将 StringBuilder 对象转换回 String 类型。
String str = “i” 与 String str = new String(“i”) 一样吗?
-
不一样,因为内存的分配方式不一样。
-
String str = “i” 的方式,Java 虚拟机会将其分配到常量池中;
-
而 String str = new String(“i”) 则会被分到堆内存中。
public class StringTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; String str3 = new String("abc"); String str4 = new String("abc"); System.out.println(str1 == str2); // true System.out.println(str1 == str3); // false System.out.println(str3 == str4); // false System.out.println(str3.equals(str4)); // true } }
在执行 String str1 = “abc” 的时候,JVM 会首先检查字符串常量池中是否已经存在该字符串对象,如果已经存在,那么就不会再创建了,直接返回该字符串在字符串常量池中的内存地址;如果该字符串还不存在字符串常量池中,那么就会在字符串常量池中创建该字符串对象,然后再返回。所以在执行 String str2 = “abc” 的时候,因为字符串常量池中已经存在“abc”字符串对象了,就不会在字符串常量池中再次创建了,所以栈内存中 str1 和 str2 的内存地址都是指向 “abc” 在字符串常量池中的位置,所以 str1 = str2 的运行结果为 true。 而在执行 String str3 = new String(“abc”) 的时候,JVM 会首先检查字符串常量池中是否已经存在“abc”字符串,如果已经存在,则不会在字符串常量池中再创建了;如果不存在,则就会在字符串常量池中创建 “abc” 字符串对象,然后再到堆内存中再创建一份字符串对象,把字符串常量池中的 “abc” 字符串内容拷贝到内存中的字符串对象中,然后返回堆内存中该字符串的内存地址,即栈内存中存储的地址是堆内存中对象的内存地址。String str4 = new String(“abc”) 是在堆内存中又创建了一个对象,所以 str 3 str4 运行的结果是 false。str1、str2、str3、str4 在内存中的存储状况
如下图所示:
String 类的常用方法都有那些?
-
getBytes():返回字符串的 byte 类型数组。
-
length():返回字符串长度。
-
equals():字符串比较。
-
replace():字符串替换。
-
substring():截取字符串。
-
toUpperCase():将字符串转成大写字符。
-
toLowerCase():将字符串转成小写字母。
String 类提供了许多常用的方法,用于处理和操作字符串。下面列举了一些常见的 String 类方法:
-
length()
:返回字符串的长度。 -
charAt(int index)
:返回指定索引位置的字符。 -
substring(int beginIndex)
:返回从指定索引开始到字符串末尾的子字符串。 -
substring(int beginIndex, int endIndex)
:返回从指定索引开始到指定索引结束的子字符串。 -
concat(String str)
:将指定字符串连接到原字符串的末尾。 -
equals(Object obj)
:比较字符串与指定对象是否相等。 -
equalsIgnoreCase(String anotherString)
:忽略大小写,比较字符串与另一个字符串是否相等。 -
toUpperCase()
:将字符串转换为大写。 -
toLowerCase()
:将字符串转换为小写。 -
trim()
:去除字符串首尾空格。 -
startsWith(String prefix)
:判断字符串是否以指定前缀开头。 -
endsWith(String suffix)
:判断字符串是否以指定后缀结尾。 -
contains(CharSequence sequence)
:判断字符串是否包含指定的字符序列。 -
indexOf(int ch)
或indexOf(String str)
:返回指定字符或字符串在字符串中第一次出现的索引。 -
lastIndexOf(int ch)
或lastIndexOf(String str)
:返回指定字符或字符串在字符串中最后一次出现的索引。 -
replace(char oldChar, char newChar)
或replace(CharSequence target, CharSequence replacement)
:替换字符串中的字符或字符串。 -
split(String regex)
:根据指定的正则表达式将字符串拆分为字符串数组。 -
startsWith(String prefix, int toffset)
或endsWith(String suffix, int toffset)
:从指定索引开始判断字符串是否以指定前缀或后缀开头或结尾。 -
isEmpty()
:判断字符串是否为空字符串。
这只是一部分常见的方法,String 类还提供了更多功能丰富的方法来满足不同的字符串处理需求。可以参考官方文档或相关学习资源了解更多详细的方法和用法。
在Java中,final 修饰 StringBuffer 后还可以 append 吗?
-
可以。final 修饰的是一个引用变量,那么这个引用始终只能指向这个对象,但是这个对象内部的属性是可以变化的。
Java 中的 IO 流的分类?说出几个你熟悉的实现类?
-
按功能来分:输入流(input)、输出流(output)。
-
按类型来分:字节流 和 字符流。
字节流:InputStream/OutputStream 是字节流的抽象类,这两个抽象类又派生了若干子类,不同的子类分别处理不同的操作类型。具体子类如下所示:
字符流:Reader/Writer 是字符的抽象类,这两个抽象类也派生了若干子类,不同的子类分别处理不同的操作类型。
字节流和字符流有什么区别?
-
数据单位不同,一个以字节,一个以字符 读写
-
字符流继承自字节流,并在其基础上添加了字符读写功能
字节流和字符流的主要区别在于处理数据的单位和处理文本数据的能力。
-
数据单位:
-
字节流以字节为单位进行读写操作,适用于处理二进制文件(如图像、音频等)或未经处理的原始数据。
-
字符流以字符为单位进行读写操作,适用于处理文本文件,可以自动处理字符编码和解码。
-
-
处理文本数据:
-
字节流不能直接处理文本数据,需要通过字符编码和解码来实现。如果使用字节流处理文本数据时,可能会出现乱码或无法正确处理多字节字符的问题。
-
字符流内置了字符编码和解码的功能,能够直接处理文本数据,并且支持不同字符编码(如UTF-8、GBK等)。
-
-
功能特性:
-
字节流提供了输入流(
InputStream
)和输出流(OutputStream
)的基本功能,用于读取和写入字节数据。 -
字符流继承自字节流,并在其基础上添加了字符读写功能,提供了更方便的字符处理方法(如
readLine()
、write()
等)。
-
总的来说,字节流适用于处理二进制数据,而字符流更适合处理文本数据并且提供了更高级的字符处理功能。在处理文本数据时,推荐使用字符流来避免字符编码的问题。
BIO、NIO、AIO是什么,有什么区别
-
BIO:Block IO,同步阻塞式 IO(传统 IO),服务实现模式为一个连接对应一个线程,即客户端发送一个连接,服务端要有一个线程来处理。如果连接多了,线程数量不够,就只能等待,即会发生阻塞。它的特点是模式简单使用方便,并发处理能力低。
-
NIO:New IO,同步非阻塞 IO(传统 IO 的升级),客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。一个线程可以处理多个连接,即客户端发送的连接都会注册到多路复用器上,然后进行轮询连接,有I/O请求就处理。
-
AIO:Asynchronous IO,异步非阻塞IO(又升级),基于事件和回调机制。引入了异步通道,采用的是proactor模式,特点是:有效的请求才启动线程,先有操作系统完成再通知服务端。
-
应用场景: BIO:适用连接数目比较小且固定的架构,对服务器要求比较高,并发局限于应用中 NIO:适用连接数目多且连接比较短的架构,如:聊天服务器,弹幕系统等,编程比较复杂 AIO:适用连接数目多且连接长的架构,如相册服务器