JavaSE(二)

2.1 面向对象基础

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

两者的主要区别在于解决问题的方式不同:

  • 面向对象会先根据问题抽象出对象,然后执行对象的方法解决问题。

  • 面向过程把解决问题的过程拆成一个个方法,然后执行这一个个方法解决问题。

Note:面向过程拆出来的一个个方法不是编程语言中的方法,而是解决问题的方法。

下面是一个求圆的面积和周长的示例,简单分别展示了面向对象和面向过程两种不同的解决方案。

面向对象

我们定义了一个 Circle 类来表示圆,该类包含了圆的半径属性和计算面积、周长的方法。

public class Circle {
    // 定义圆的半径
    private double radius;

    // 构造函数
    public Circle(double radius) {
        this.radius = radius;
    }

    // 计算圆的面积
    public double getArea() {
        return Math.PI * radius * radius;
    }

    // 计算圆的周长
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }

    public static void main(String[] args) {
        // 创建一个半径为3的圆
        Circle circle = new Circle(3.0);

        // 输出圆的面积和周长
        System.out.println("圆的面积为:" + circle.getArea());
        System.out.println("圆的周长为:" + circle.getPerimeter());
    }
}

面向过程

我们直接定义了圆的半径,并使用该半径直接计算出圆的面积和周长。

public class Main {
    public static void main(String[] args) {
        // 定义圆的半径
        double radius = 3.0;

        // 计算圆的面积和周长
        double area = Math.PI * radius * radius;
        double perimeter = 2 * Math.PI * radius;

        // 输出圆的面积和周长
        System.out.println("圆的面积为:" + area);
        System.out.println("圆的周长为:" + perimeter);
    }
}

2.创建一个对象用什么运算符? 对象实例与对象引用有何不同?

创建一个对象用 new 运算符,对象实例是堆内存中一个具体的对象,对象引用是指向对象实例的一个内存地址。 

3.对象相等和引用相等的区别

  • 对象相等是指内存中存放的内容相等

  • 引用相等是指对象在内存中的地址相等

这里举一个例子:

String str1 = "hello";
String str2 = new String("hello");
String str3 = "hello";
// 使用 == 比较字符串的引用相等
System.out.println(str1 == str2);
System.out.println(str1 == str3);
// 使用 equals 方法比较字符串的相等
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));

输出结果:

false
true
true
true

从上面的代码输出结果可以看出:

  • str1 和 str2 不相等,而 str1 和 str3 相等。这是因为 == 运算符比较的是字符串的引用是否相等。

  • str1、str2、str3三者的内容都相等。这是因为 equals 方法比较的是字符串的内容,即使这些字符串的对象引用不同,只要它们的内容相等,就认为它们是相等的。

4.什么是构造方法?构造方法有哪些特点?是否可以被 override?

构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。构造方法特点如下:

  • 方法名与类名相同。

  • 没有返回值,也不能用 void 声明构造函数。

  • 生成类的对象时自动执行,无需调用。

构造方法不能被重写,但是可以重载。

5.如果一个类没有声明构造方法,该程序能正确执行吗?

如果一个类没有声明构造方法也会有默认的无参构造方法。如果我们自己添加了类的构造方法,无论是否有参,Java 就不会添加默认的无参构造方法了。所以一个类没有声明构造方法,程序也能正确执行。

我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号,因为要调用无参的构造方法。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来,无论是否用到,因为这可以帮助我们在创建对象的时候少踩坑。

6.面向对象三大特征

封装

封装是指把一个对象的状态信息,也就是属性隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些能够被外界访问的方法来操作属性。

如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

public class Student {
    private int id;//id属性私有化
    private String name;//name属性私有化

    //获取id的方法
    public int getId() {
        return id;
    }

    //设置id的方法
    public void setId(int id) {
        this.id = id;
    }

    //获取name的方法
    public String getName() {
        return name;
    }

    //设置name的方法
    public void setName(String name) {
        this.name = name;
    }
}

继承

继承是指一个类可以得到另一个类的属性和方法。而且通过继承,子类不仅可以得到父类的成员变量和成员方法,也可以添加自己的成员变量和成员方法。继承的好处就是在父类中就可以定义子类的共同点提高代码的复用性,而子类也可以定义额外的特性使得自己与众不同。

继承的特点

  • 子类拥有父类对象所有的属性和方法,包括私有属性和私有方法,但是父类中的私有属性和方法子类是无法访问,只是拥有

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

  • 子类可以重写父类的方法。

多态

多态表示一个对象具有多种状态,具体表现为父类引用指向子类对象。

多态的特点

  • 对象类型和引用类型之间具有继承或实现的关系;

  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;

  • 多态不能调用“只在子类存在但在父类不存在”的方法;

  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

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

共同点

  • 都不能被实例化。

  • 都可以包含抽象方法。

  • 都可以有实现了的方法

区别

  • 接口主要用来对类的行为进行约束,当一个类实现了某个接口就具有了对应的行为。抽象类主要用来提高代码复用。

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

  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值;抽象类中成员变量的访问控制修饰符默认是缺省的,可以在子类中重新定义,也可被重新赋值。

Note:Java 8 以后可以用 default 关键字在接口中定义默认方法。

8.浅拷贝、深拷贝、引用拷贝了解吗?

  • 浅拷贝:浅拷贝会在堆上创建一个新对象。但是,如果原对象内部有引用类型的属性,浅拷贝只会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。

  • 深拷贝:深拷贝会完全复制整个对象,包括原对象所包含的内部对象。

  • 引用拷贝:引用拷贝就是将一个变量的引用地址拷贝一份给另一个变量,也就是两个不同的变量指向同一个对象。

下面这张图用来描述浅拷贝、深拷贝、引用拷贝:

2.2 Object

1.Object 类的常见方法有哪些?

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

/**
 * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
 */
public final native Class<?> getClass()
/**
 * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的 HashMap。
 */
public native int hashCode()
/**
 * 用于比较 2 个对象的地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * native 方法,用于创建并返回当前对象的一份浅拷贝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 用于返回一个描述对象的字符串表示,默认的 toString()方法会返回一个由类的名称、@ 符号和
 * 对象的哈希码组成的字符串。建议 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 { }

在Java中,每个对象都有一个与之关联的对象监视器(也称为锁)。对象监视器实际上是存储在对象头中的一部分数据,它记录了该对象被锁定的状态、持有锁的线程 ID、等待锁的线程队列等信息。当一个线程通过 synchronized 关键字获取了一个对象的锁之后,它就可以调用该对象上的 notify() 或 notifyAll() 方法来唤醒等待队列中的线程。这样做的目的是为了确保在并发环境下,只有持有锁的线程才能控制等待队列中的线程的执行。

2.== 和 equals() 的区别

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

  • 对于基本数据类型来说,== 比较的是值。

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

equals 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。

equals 方法存在于 Object 类中,而 Object 类是所有类的直接或间接父类,因此所有的类都有equals 方法。Object 类 equals 方法如下:

public boolean equals(Object obj) {
     return (this == obj);
}

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

  • 类没有重写 equals 方法,默认使用的是 Object 类 equals 方法,比较的是二个对象的内存地址是否相等。

  • 类重写了 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

String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

String 类的 equals 方法:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

3.hashCode() 方法有什么用?

hashCode 方法的作用是获取对象的哈希码。而哈希码的作用是确定对象在哈希表中的索引位置。hashCode 定义在 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode 函数。

Object 的 hashCode 方法是本地方法,也就是用 C 语言或 C++ 实现的。

4.为什么要有 hashCode?

我们以 HashSet 如何检查重复为例子来说明为什么要有 hashCode?

HashSet的原理:

在Java中,HashSet 是基于哈希表实现的集合,用于存储唯一的元素。当向 HashSet 集合中添加引用类型的对象时,会先检查哈希表中是否存在与该对象的哈希码相同的哈希桶。如果存在哈希码相同的桶,则会用 equals 方法比较这些桶中已有的元素与要添加的对象的内容。如果两个对象的哈希码相同但内容不同,那么它们会被视为不同的元素,并且会被加入到同一个哈希桶中。在哈希桶中,元素的添加顺序可能由具体的实现方式决定,可以是链表、红黑树等。例如,在Java 8之前,使用的是数组+链表的结构,而在Java 8及以后的版本中,当哈希桶中的元素数量超过一定阈值时,会将链表转换为红黑树以提高性能。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

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

那为什么 JDK 还要同时提供这两个方法呢?

这是因为在一些容器,比如 HashMap、HashSet 中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高,参考添加元素进 HashSet 的过程。

我们在前面也提到了添加元素进 HashSet 的过程,如果 HashSet 在对比的时候,同样的 hashCode有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。

那为什么不只提供 hashCode() 方法呢?

这是因为两个对象的 hashCode 值相等并不代表两个对象就相等。

那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?

因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关,所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode。

总结下来就是:

  • 如果两个对象的 hashCode 值相等,那这两个对象不一定相等。
  • 如果两个对象的 hashCode 值相等并且 equals()方法也返回 true,我们才认为这两个对象相等。
  • 如果两个对象的 hashCode 值不相等,我们就可以直接认为这两个对象不相等。

相信在看了上面对 hashCode() 和 equals() 的介绍之后,下面这个问题已经难不倒你们了。

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

因为相等的二个对象,不仅要通过 equals 方法判断出两个对象相等,这二个对象的 hashCode 值必须也相等。如果一个类只重写了 equals 方法,比如重写完后的 equals 方法是根据对象的内容判断是否相等,那 hashCode 方法也应该重写。否则,当想要把这个类的二个对象映射到哈希表等数据结构时,可能会导致用 equals 方法判断出了二个对象相等,但因为这二个对象的哈希码不同,并不会被正确映射到同一个桶中。

总结

  • equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

  • 两个对象有相同的 hashCode 值,他们也不一定是相等的。

在Object类中,equals方法是根据对象的内存地址来判断二个对象是否相等,hashCode方法也是根据对象的内存地址生成哈希码。如果重写后的equals方法是根据对象的内容判断是否相等,那hashCode方法也应该重写为根据对象的内容生成哈希码,以保持一致。

2.3 String

1.String 类型的字符串为什么是不可变的?

String 类中使用 final 关键字修饰字符数组来保存字符串,final 修饰符保证了字符数组在创建后不能再改变,从而确保了字符串的内容不会被修改。

public final class String 
implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
	//...
}

在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串。

public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
    // @Stable 注解表示变量最多被修改一次,称为“稳定的”。
    @Stable
    private final byte[] value;
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    byte[] value;

}

2.String、StringBuffer、StringBuilder 的区别?

可变性

String 是不可变的。StringBuffer 与 StringBuilder 都继承自 AbstractStringBuilder 类,在AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法,比如 append 方法。所以,StringBuffer 与 StringBuilder 都是可变的。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
  	//...
}

线程安全性

String 中的对象是不可变的,是线程安全的。StringBuffer 对方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法加同步锁,所以是非线程安全的。

性能

每次对 String 对象进行改变时,都会生成一个新的 String 对象,并将对象引用指向新的 String 对象。StringBuffer 和 StringBuilder 每次都是对对象本身进行操作,而不是生成新的对象并改变对象引用。因此,三者之中,String 性能最差,StringBuffer 较好,StringBuilder 最好。

3.Object 的 equals 方法和 String 的 equals 方法有何区别?

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

4.字符串常量池的作用了解吗?

字符串常量池是JVM为了减少字符串对象的重复创建而维护的一块内存空间,存储的是字符串对象,并且存储的字符串对象都是唯一的。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

5.String s = new String("abc"); 这句话创建了几个字符串对象?

会创建 1 或 2 个字符串对象。

创建二个字符串对象:如果字符串常量池中不存在字符串对象"abc",那么将首先在字符串常量池中创建字符串对象"abc",然后在堆空间中创建第 2 个字符串对象,并将第二个字符串对象的地址赋值给 s。

创建一个字符串对象:如果字符串常量池中已经存在字符串对象"abc",则只会在堆中创建 1 个字符串对象"abc"。

关于串池存的到底是对象还是引用,下面是网上比较可信的一个版本

在 JDK 6 及以前版本,字符串常量池保存字符串对象;JDK 6 之后的版本中,既保存了字符串对象,又保存了字符串对象的引用。

  • 在 JDK 6 中,当调用字符串的 intern() 时,若字符串常量池先前已创建出该字符串对象,则返回字符串常量池中该字符串对象的引用。否则,将该字符串对象添加到字符串常量池中,再返回该字符串对象的引用。

  • 而在 JDK 7 中,当调用 intern() 时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,若该字符串对象已经存在于 Java 堆中,则将堆中对此对象的引用添加到字符串常量池中,然后返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。

String a = new String("abc") 的原理

当我们通过 String a = new String("abc"); 创建一个字符串对象时,会检查字符串常量池中是否存在字符串对象"abc"的引用地址,

  • 如果存在,就直接在堆内存中创建一个新的字符串对象"abc",并将对象地址赋值给a,

  • 如果不存在,则先在字符串常量池中创建一个字符串对象"abc",然后继续在堆内存创建一个字符串对象"abc",并把这个对象的地址赋值给变量a。

6.String.intern 方法有什么作用?

String 的 intern 方法的作用是将指定的字符串对象保存在字符串常量池中,可以简单分为两种情况:

  • 如果字符串常量池中保存了指定的字符串对象,intern 方法就直接返回该对象的引用。

  • 如果字符串常量池中没有保存字符串对象,那就在字符串常量池中创建一个字符串对象,并把引用并返回。

7.String 类型的变量和常量做 + 运算时发生了什么?

先来看字符串不加 final 关键字拼接的情况,对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。

在 JDK 1.8 下的示例代码:

String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

在编译过程中,Javac 编译器会进行一个叫做 常量折叠 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一,因为代码优化几乎都在即时编译器中进行。

对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string"; 。

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以,比如以下类型的常量:

  • 基本数据类型的字面量以及字符串字面量。

  • final 修饰的基本数据类型变量和字符串变量

  • 字符串通过 + 拼接得到的字符串、基本数据类型之间加减乘除运算、基本数据类型的位运算

字符串常量和对象引用进行拼接时,实际上是通过 StringBuilder 调用 append 方法实现的,拼接完成之后调用 toString 方法得到一个 String 对象 。所以我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。

String str4 = new StringBuilder().append(str1).append(str2).toString();

字符串变量使用 final 关键字声明之后,可以让编译器当做常量来处理。编译器在程序编译期就可以确定它的值。

示例代码:

final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true

如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。

示例代码,str2 在运行时才能确定其值:

final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
      return "ing";
}

Note:

  • 引用类型的变量在程序编译期是无法确定的,编译器无法对其进行优化。
  • 这里的常量不是指 static final 修饰的那个,指的是字面量
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

真滴book理喻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值