社招又来学习 Java 啦,这次选了何昊老师的程序员面试笔记作为主要资料,记录一下一些学习过程。
1.1 Java 程序初始化
Java 程序初始化遵循规则:静态变量优于动态变量;父类优于子类;成员变量的定义顺序;
具体体现为:父类静态变量 -> 父类静态代码块 -> 子类静态变量 -> 子类静态代码 -> 父类非静态变量 -> 父类非静态代码块 -> 父类构造方法 -> 子类非静态变量 -> 子类非静态代码块 -> 子类构造方法。
1.2 构造方法
- 每个类可以有多个构造方法,当没有提供时,编译器在将源代码编译为字节码的时候会提供一个默认的没有参数的构造方法。
- 仅在对象实例化时调用,不可显示调用。
- 不可以被继承,也就无法被重写。但可以被重载:重写(Override)是指子类有一个方法与其父类中的某个方法具有相同的方法名、返回类型和参数列表。然而,构造方法不能被重写,因为它们没有返回类型,并且其名称必须与类名相同,这使得子类无法声明一个与父类构造方法签名相同的方法。Java设计者选择不允许构造方法被继承或重写,主要是为了避免构造过程中的复杂性和潜在的错误。构造方法的调用必须明确无误,以确保对象在使用前被正确初始化。
- 子类可以通过 super 关键字显式调用父类的构造方法;实例化对象时,首先会调用父类的构造方法再执行子类的构造方法;
1.3 clone 方法的作用
Java 在处理基本数据类型时采用值传递的方式,其他类型都是按引用传递。clone 提供从某个已有的对象创造出一个和此对象相同状态的对象的方法。
clone 的类继承 cloneable 接口(实际无任何接口方法,仅为标识),重写 Object 类中的 clone 方法,在 clone 方法里调用 super.clone()(最终都会调用到 object 的 clone 方法,这个方法返回 object 对象的一个拷贝),最终将浅拷贝的引用指向对象新的克隆体。
默认的 clone 方法仅提供了浅克隆,实现深克隆通常比实现浅克隆更复杂,因为深克隆需要递归地复制对象的所有引用对象。以下是实现深克隆的一些方法:
-
重写
clone()
方法:如果你的类中的对象引用了其他对象,并且你想要复制这些对象,你需要重写clone()
方法,并在这个方法中确保所有引用的对象也被复制。 -
使用序列化:一种实现深克隆的简单方式是使用对象序列化机制。通过将对象序列化到一个字节数组,然后再从该字节数组反序列化,可以创建对象的一个完整副本。
-
复制构造函数:创建一个复制构造函数,它接受一个原对象作为参数,并使用该对象的值来初始化新对象。这种方法需要确保所有引用的对象也通过复制构造函数被复制。
-
复制工厂方法:类似于复制构造函数,但使用一个静态方法来创建对象的副本。
-
使用Apache Commons Lang:如果你使用Apache Commons Lang库,可以使用
SerializationUtils.clone()
方法来实现深克隆。
在Java中,数组的克隆行为取决于数组的维度:
-
一维数组:当克隆一维数组时,会进行浅克隆。这意味着克隆操作会创建一个新的数组对象,但数组中的元素不会被克隆。如果数组元素是引用类型,那么克隆后的数组将引用与原始数组相同的对象。
-
多维数组:对于多维数组,克隆行为取决于每个维度的数组。每个维度的数组都是作为一个单独的一维数组来克隆的,因此每个维度的数组都是浅克隆的。这意味着,如果克隆一个二维数组,你会得到一个新的二维数组对象,但每个子数组仍然是原始子数组的引用。
如果你需要对数组进行深克隆,即数组及其所有元素都是全新的独立副本,你需要手动实现这一过程。对于一维数组,你可以创建一个新数组并复制每个元素;对于多维数组,你需要递归地复制每个子数组。
1.3 补充 - 访问级别
在Java中,public
、protected
和private
是访问修饰符,它们控制类成员(包括字段、方法和构造函数)的访问级别。以下是这三个访问修饰符的区别:
-
public(公共的):
- 成员可以被任何其他类访问,不受限制。
- 无论是同一个包中的类还是不同包中的类,都可以访问
public
成员。
-
protected(受保护的):
- 成员可以被同一个包中的其他类访问,也可以被不同包中的子类访问。
- 访问级别比
public
更受限,因为不同包中的非子类不能访问protected
成员。
-
private(私有的):
- 成员只能在其所在的类内部访问。
private
成员不能被同一个包中的其他类访问,也不能被子类访问(即使子类在同一包中)。private
是访问级别最受限的修饰符。
以下是一些额外的考虑:
-
默认访问级别:如果既不使用
public
、protected
,也不使用private
修饰,类成员具有默认访问级别。默认访问级别意味着成员只能被同一个包中的其他类访问,但不能被子类访问(即使子类在同一包中)。 -
访问控制:访问修饰符是Java实现封装的一种方式。通过限制对类成员的访问,可以隐藏类的内部实现细节,只暴露必要的操作界面。
-
继承和访问级别:当子类继承父类时,只能继承
public
和protected
成员。private
成员不能被继承,因为它们不可见。 -
类访问:类本身也可以使用这些访问修饰符。
public
类可以被任何其他类访问,而private
类只能在定义它的类加载器中访问。
1.4 反射
反射机制是指对于处在运动状态的类,都可以获取到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法及访问他的属性。具体为对任何一个类都可以得到对应的 Class 实例,通过 Class 实例就可以获取类或者对象的所有信息。
-
设计原则:反射的使用应该遵循Java的设计原则,即封装应该被尊重。滥用反射来访问私有成员可能会破坏类的封装性,使代码难以理解和维护。
-
使用场景:反射通常用于框架和库的开发,例如依赖注入框架、ORM(对象关系映射)框架等,这些框架需要访问目标类的私有成员以实现其功能。
1.5 Lambda 表达式
Lambda 表达式允许把函数作为一个方法的参数,即匿名函数。
1.6 多态的实现机制
多态意为同一个操作作用在不同的对象的时候会出现不同的语义,例如"+" 的操作。主要表现形式为
- 重载:同一个类中不同参数列表的同名方法,在编译时决定调用。
- 重写:子类可以重写父类的方法,运行时才计算用哪个方法。
1.8 抽象类与接口的异同
一个类中包含抽象方法即为抽象类;抽象类在使用时不能被实例化。其设计思想为继承,is - a 关系。
接口是方法的集合,可以设置默认实现;强调功能的实现,has a 关系
1.8 补充 - final 对象
在Java中,final
变量表示一旦变量被初始化赋值之后,就不能再给它赋新的值。final
变量可以是类的成员变量,也可以是局部变量。以下是final
变量的一些关键特性:
-
不可变性:
final
变量的值是不可变的。这意味着一旦给final
变量赋了初值,就不能再修改它。 -
必须初始化:
final
变量必须在声明时或者在构造器中被初始化。对于类的成员变量,如果它们是final
的,并且在声明时没有显示初始化,那么它们必须在每个构造器中被初始化。 -
引用的不变性:如果
final
变量是一个引用,那么这个引用一旦指向一个对象,就不能指向另一个对象。但是,被引用的对象本身的字段是可以被修改的(除非对象本身是不可变的)。 -
局部变量:在方法中,你可以使用
final
来声明一个局部变量,表示这个方法不会改变这个变量的值。 -
参数:方法的参数也可以是
final
的,这表示方法内部不会改变参数的值。 -
匿名内部类:在使用匿名内部类时,如果外部类的成员变量是
final
的,那么这些变量可以在匿名内部类中安全使用,因为它们不会被改变。 -
常量:通常使用
final
来声明常量,因为常量的值不应该被改变。 -
性能优化:由于
final
变量的不可变性,编译器和运行时环境可以对它们进行优化。
1.8 补充 - 静态方法
静态方法(Static Method)是与类本身相关联,而非与类的任何特定对象相关联的方法。在Java中,静态方法使用static
关键字进行声明。以下是静态方法的一些关键特性:
-
调用方式:静态方法可以通过类名直接调用,而不需要创建类的实例。例如:
ClassName.methodName()
。 -
类级别:静态方法属于类级别,这意味着它们与类的所有实例共享,而不是与单个对象关联。
-
访问类变量:静态方法可以访问类的静态变量,但它们不能直接访问类的非静态成员变量或方法,除非通过对象引用。
-
构造函数:静态方法不能被重写(Override),因为它们不属于类的实例,但可以被再次声明(Overload)。
-
实例化:静态方法在类加载时即被初始化,不需要类实例化。
-
工厂模式:静态方法常用于实现工厂模式,用于创建类的实例,例如:
return new Constructor()
。 -
工具类:静态方法常用于工具类(Utility Class),提供一组静态工具方法,这些方法通常执行通用操作,与类的实例无关。
-
单例模式:静态方法在实现单例模式时也非常重要,用于控制实例的创建。
-
静态初始化块:静态方法可以与静态初始化块(Static Initializer Block)一起使用,以执行类加载时的初始化操作。
1.9 break \ continue \ return
break:强制跳出当前循环;仅终止当前嵌套;
continue:停止当前循环,进入下次循环;
return:从方法返回;
1.10 switch
仅支持枚举常量:int \ Integer \ byte \ short \ char(可以隐式的转换为 int 类型)。Java 7 后支持 String,本质是将其进行hash,得到一个int 类型的 hash值进行比较,匹配成功后还会调用 String.equals 方法进行判断。
1.11 volatile
volatile 的使用是为了线程安全但不保证线程安全。线程安全的三个要素:可见性、有序性和原子性。
-
原子性(Atomicity):
- 原子性是指一个操作或者一系列操作要么全部执行,要么全部不执行,不会出现中间状态。
- 在多线程环境中,为了确保操作的原子性,通常需要使用同步机制,如
synchronized
关键字、锁(Lock
接口及其实现类)、原子变量类(如AtomicInteger
)等。
-
可见性(Visibility):
- 可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。
- Java内存模型(JMM)规定了变量的修改和访问规则。为了确保可见性,可以使用
volatile
关键字、锁、以及一些并发API,如ThreadLocal
。
-
有序性(Ordering):
- 有序性是指程序执行的顺序需要符合逻辑上的先后关系。
- 在多线程环境中,由于编译器优化、处理器乱序执行等原因,指令重排序可能导致线程安全问题。为了确保有序性,可以使用
volatile
关键字来禁止指令重排序,或者使用synchronized
和锁来保证代码块的执行顺序。
可以解决:缓存副本、指令重排序,但不可解决原子性问题(当一个线程执行写的时候才会修改标记)。synchoronized 关键字可以解决多线程的原子操作问题。
以下场景不需要保证原子性,仅需要提供可见性和有序性:
-
状态标志:如果一个变量仅用作标志,以指示某个状态或条件,而这个状态或条件的变更本身不涉及复杂的操作。
-
监控变量:在某些监控系统中,可能需要记录某个事件发生的次数或状态,而这些记录操作不需要复合操作的原子性。
-
控制变量:用于控制线程行为的变量,如启动标志、停止标志等,这些标志的状态改变不需要复杂的同步操作。
-
观察者模式:在观察者模式中,观察者的状态可能需要立即被主题所知,而不需要执行复合的原子操作。
-
日志记录:在多线程应用中,日志记录可能使用
volatile
变量来确保日志信息的可见性,尽管写入操作本身可能不是原子的。 -
简单的状态通知:当一个变量用于在线程间传递简单的状态信息,而不需要执行复合操作时。
-
非核心数据:对于那些不会引起严重后果的非核心数据,即使出现偶尔的数据不一致,也可以接受。
1.12 Java 基本数据类型
基本类型的数据变量在声明之后就会立刻在栈上分配内存空间,除以上类型外,其余都是引用类型。
1.14 值传递和引用传递的区别
值传递:方法调用中实参把值传给形参。形参仅是一个临时变量。
引用传递:传递对象的地址,形参和实参指向同一个存储单元,因此对形参的修改会影响实参的值。
1.16 字符串
JVM 中存在一个字符串常量池,可以被共享使用。从 JDK 1.7 开始字符串常量池从 Perm 移动到堆区了。
- 直接通过双引号声明的对象会放在常量池中;
- 通过 String 提供的 intern 方法会放入常量池中;
1.16 补充 - 内存泄露
通常指认为一个对象会被垃圾回收期收集,但是由于某种原因垃圾回收期器无法回收这个对象。eg: 1.6 版本之前的 substring 方法。
1.17 == \ equals \ hashCode
==:比较变量对应的内存中存储的数值是否相同。对于基础类型可以直接比较,对于引用类型只能判断是否指向同一块存储空间。
equals: 没有复写的情况下等于 ==;用于自定义两个类何时是相同的。
hashCode:Object类提供的将内存地址转换成一个int值,所以当没有重写时,任何对象的 hashcode 都不相同。一般覆盖 equals 也需要覆盖 hashcode
1.18 String \ Stringbuffer \ StringBuilder \ StringTokenizer
String :不可变类。适用于共享。修改时会首先创建一个 StringBuilder。
StringBuffer:适用于一个字符串常被修改的场景。线程安全。
StringBuilder:类似 StringBuffer,线程不安全。
StringTokenizer:用于分割字符串。
1.19 finally
由于程序执行 return 时意味着当前方法的结束,任何语句只能在 return 之前执行(除开 exit),finnaly 也在 Return 之前结束且覆盖其他 return 语句。并且 return 时会复制变量的值。
- 首先执行 return 的语句但不返回,然后执行 finnally 块,最后才返回。
1.20 try with resource
在 Java 中,try-with-resources
特性可以应用于实现了 java.lang.AutoCloseable
接口或其子接口 java.io.Closeable
的对象。以下是一些常见的实现了这些接口的对象类型:文件、管道、数据库等。
1.21 补充 - 异常
Java的异常分为两大类:
- 编译时异常(Checked Exceptions):这些异常需要在代码中进行显式处理,通常是通过
throws
关键字在方法签名中声明,或者在方法内部使用try-catch
块捕获并处理。 - 运行时异常(Runtime Exceptions):这些异常是程序逻辑错误导致的,比如空指针异常(
NullPointerException
)、数组越界异常(ArrayIndexOutOfBoundsException
)等。它们是RuntimeException
类的子类,不需要强制处理。
1.21 补充 - 金额
可以用 BigDecimal 来表示金额,因为 float 和 double 会存在丢失精度的问题。并且需要使用 string 类型进行调用。
1.21 补充 - 实现线程安全
方法一:使用提供了安全能力的原子变量例如 AtomicInteger;
方法二:用 sychronized 修饰方法,保证同一时刻只有一个线程调用;
方法三:手动对操作加锁解锁;