Java基础2(补档)


面向对象基础

什么是面向对象的程序设计方法

面向对象是一种更优秀的程序设计方法,它的基本思想是使用类、对象、继承、封装等基本概念进行程序设计。

它从现实世界中客观存在的事物出发来构造软件系统,并在系统构造中尽可能运用人类的自然思维方式,强调直接以现实世界中的事物为中心来思考,认识问题,并根据这些事物的本质特点,把它们抽象地表示为系统中的类,作为系统的基本构成单元,这使得软件系统的组件可以直接映像到客观世界,并保持客观世界中事物及其相互关系的本来面貌。

面向对象和面向过程的区别

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

面向对象开发的程序更易维护、易复用、易扩展。

面向对象的三大特征

面向对象的程序设计方法具有三个基本特征:封装、继承、多态。

封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能

继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法

多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。(父类的引用指向子类的实例)

接口和抽象类有什么共同点和区别

共同点

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

区别

  1. 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  2. 一个类只能继承一个类,但是可以实现多个接口。
  3. 接口不能为普通方法提供方法实现;抽象类可以包含普通方法。
  4. 接口中的成员变量只能是 public static final 类型的静态常量,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
  5. 接口里不能包含初始化块;但抽象类可以包含初始化块。
  6. 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。

深拷贝和浅拷贝区别了解吗?什么是引用拷贝?

实现 Cloneable 接口并重写 Object 类中的 clone() 方法 来实现对象的克隆

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
  • 引用拷贝:两个不同的引用指向同一个对象。

Object类

Object 类的常见方法有哪些?

Object 类是一个特殊的类,是所有类的父类。

/**
 * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
 */
public final native Class<?> getClass()
/**
 * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
 */
public native int hashCode()
/**
 * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * naitive 方法,用于创建并返回当前对象的一份拷贝。
 */
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 { }

== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

equals() 方法存在两种使用情况:

  • 类没有重写 equals()方法:通过equals()比较该类的两个对象时,等价于通过“ == ”比较这两个对象,使用的默认是 Objectequals()方法。
  • 类重写了 equals()方法:一般我们都重写 equals()方法来比较两个对象中的属性是否相等;
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true

hashCode() 有什么用

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

为什么要有 hashCode

hashCode() 和 equals()都是用于比较两个对象是否相等。

  • 如果两个对象相等,则它们必须有相同的哈希码。
  • 如果两个对象有相同的哈希码,则它们未必相等。

以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCodeHashSet 会假设对象没有重复出现。

但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。

这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

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

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

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

重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题?

  • 同一个对象在哈希表中根据默认的hashCode()得到不同的哈希码,相等的对象在哈希表中无法正确地进行查找和比较。
  • 不同的对象可能被映射到相同的哈希桶中,将导致哈希表的性能下降

String类

String类的常用方法

  • char charAt(int index):返回指定索引处的字符;
  • String substring(int beginIndex, int endIndex):从此字符串中截取出一部分子字符串;
  • String[] split(String regex):以指定的规则将此字符串分割成数组;
  • String trim():删除字符串前导和后置的空格;
  • int indexOf(String str):返回子串在此字符串首次出现的索引;
  • int lastIndexOf(String str):返回子串在此字符串最后出现的索引;
  • boolean startsWith(String prefix):判断此字符串是否以指定的前缀开头;
  • boolean endsWith(String suffix):判断此字符串是否以指定的后缀结尾;
  • String toUpperCase():将此字符串中所有的字符大写;
  • String toLowerCase():将此字符串中所有的字符小写;
  • String replaceFirst(String regex, String replacement):用指定字符串替换第一个匹配的子串;
  • String replaceAll(String regex, String replacement):用指定字符串替换所有的匹配的子串。

String 为什么是不可变的?

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

String 为什么要设计为不可变类

(1)字符串常量池的需要:字符串常量池是 Java 堆内存中一个特殊的存储区域, 当创建一个 String 对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;

(2)允许 String 对象缓存 HashCode:Java 中 String 对象的哈希码被频繁地使用, 比如在 HashMap 等容器中。字符串不变性保证了 hash 码的唯一性,因此可以放心地进行缓存。

(3)安全:String 被许多的 Java 类(库)用来当做参数,例如:网络连接地址 URL、文件路径 path、还有反射机制所需要的 String 参数等, 假若 String 不是固定不变的,将会引起各种安全隐患。

如何设计一个不可变的类

  1. 将类声明为final:通过将类声明为final,禁止其他类继承该类从而修改其行为。
  2. 声明所有字段为private final:将所有字段声明为私有和不可变的,通过使用private关键字限制对字段的直接访问,并使用final关键字确保字段在对象创建后不能被修改。
  3. 不提供修改字段的公共方法:避免提供任何公共方法(如setter)来修改字段的值。如果需要提供对字段的访问,可以提供只读(get)方法来返回字段的副本或变量。
  4. 保护性拷贝:如果类中包含可变对象的引用,需要进行保护性拷贝。即在返回可变对象引用之前,复制一份不可变的副本并返回。这样可以防止外部代码修改内部的可变对象。
  5. 构造函数初始化所有字段:通过构造函数初始化所有字段,并确保在对象创建后字段的值不会改变。
  6. 如果类中包含集合类型的字段,应使用不可变集合(如Collections.unmodifiableList)或者使用私有字段和访问方法来防止外部代码修改集合。
  7. 重写equals()hashCode()方法:要正确比较两个不可变对象,需要重写equals()hashCode()方法,以确保对象的内容相同时返回相等的结果。

Java 9 为何要将String的底层实现由 char[] 改成了 byte[]

在大部分汉字的编码中,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。

String#equals() 和 Object#equals() 有何区别

String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Objectequals 方法是比较的对象的内存地址。

String和StringBuffer的区别

String类是不可变类,每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。

StringBuffer是一个字符序列可变的字符串

当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。

StringBuffer和StringBuilder的区别

相同点:

StringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类 AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。

不同点:

  • StringBuffer是线程安全的,而StringBuilder是非线程安全的,StringBuffer类中的方法都添加了synchronized关键字,也就是给这个方法添加了一个锁,用来保证线程安全。
  • StringBuilder性能略高。

字符串拼接的方法有哪些

  1. + 运算符:如果拼接的都是字符串直接量,则适合使用 + 运算符实现拼接;
  2. StringBuilder:如果拼接的字符串中包含变量,并不要求线程安全,则适合使用StringBuilder;
  3. StringBuffer:如果拼接的字符串中包含变量,并且要求线程安全,则适合使用StringBuffer;
  4. String类的concat方法:如果只是对两个字符串进行拼接,并且包含变量,则适合使用concat方法;

+ 运算符

  • 字符串直接量拼接:编译器会将其直接优化为一个完整的字符串,直接写一个完整的字符串是一样的,所以效率非常的高。
  • 包含变量的拼接:编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()方法拼接,之后调用 toString() 得到一个 String 对象,效率也很高。

StringBuilder/StringBuffer类

StringBuilder/StringBuffer都有字符串缓冲区,缓冲区的容量在创建对象时确定,并且默认为16。当拼接的字符串超过缓冲区的容量时,会触发缓冲区的扩容机制,即缓冲区加倍。

String类的concat方法

先创建一个足以容纳待拼接的两个字符串的字节数组,然后先后将两个字符串拼到这个数组里,最后将此数组转换为字符串。

只拼接2个字符串时,concat方法的效率要优于StringBuilder。

String a = “abc”; 创建过程,存放位置

JVM会使用常量池来管理字符串直接量。在执行这句话时,JVM会先检查常量池中是否已经存有”abc”,若没有则将”abc”存入常量池,否则就复用常量池中已有的”abc”,将其引用赋值给变量a。

new String(“abc”) ; 创建过程,存放位置

在执行这句话时,JVM会先使用常量池来管理字符串直接量,即将”abc”存入常量池(若已存在就不用再存了)。然后再创建一个新的String对象,这个对象会被保存在堆内存中。并且,堆中对象的数据会指向常量池中的直接量。

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

文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录