首页 > 其他分享 >面向对象

面向对象

时间:2022-11-02 15:00:15浏览次数:29  
标签:String 对象 子类 面向对象 字符串 父类 方法

面向对象

1. 面向对象程序设计概述

面向对象程序设计(Object Oriented Programming,OOP)作为一种新方法,其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。

面向对象程序设计方法是尽可能模拟人类的思维方式,使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程,也即使得描述问题的问题空间与问题的解决方案空间在结构上尽可能一致,把客观世界中的实体抽象为问题域中的对象。

面向对象程序设计以对象为核心,该方法认为程序由一系列对象组成。类是对现实世界的抽象,包括表示静态属性的数据和对数据的操作,对象是类的实例化。对象间通过消息传递相互通信,来模拟现实世界中不同实体间的联系。在面向对象的程序设计中,对象是组成程序的基本模块。

正因如此,OOP 达到了软件工程的三个主要目标:重用性、灵活性和扩展性。

面向对象有三大特征:

  1. 封装:是指将数据以及与这个数据相关的操作组装到一起,一并装在一个“模块”中(也就是一个类中)。

    这样一来,对于用户来说,对象是如何对各种行为进行操作、运行、实现等细节是不需要了解清楚的,用户只需要通过模块提供的对外接口进行相关方面的操作即可

  2. 继承:指的是通过拓展一个类来创建一个新类的过程,被拓展的类通常称作父类,拓展出来的新类通常被称作子类。

    对于开发者来说,如果两个类存在共性,那么可以把共性抽象出来,变成这两个类的父类,这也是我们提倡的抽象的思维。同时,继承也大大减少了创建一个和其他类存在共性的新类的成本。

    在 Java 中,所有的类都有一个共同的祖先类 Object

  3. 多态:从宏观上看,多态是指在面向对象技术中,当不同的多个对象同时接收到同一个完全相同的消息之后,所表现出来的动作是各不相同的,具有多种形态;从微观上看,多态指的是父类变量可以指向任意子类对象。

    多态对已存在代码具有可替换性,同时具有可拓展性,增加新的子类不影响已存在类的多态性、继承性。

1.1 类之间的关系

类与类之间一般有五种关系,从上到下耦合度依次升高:

  1. 依赖,通俗的讲就是一个类使用了另一个类。体现为一个类是另一个类的方法中的参数,局部变量。
  2. 关联,关联分为单向关联和双向关联(甚至还有自关联),体现为一个类的对象被作为另一个类的字段。(如果另一个类也有该类的字段,则是双向关联)
  3. 聚合,是关联的一种,表现形式上和关联一致。唯一区别体现在语义上:聚合的两个类有一种整体和局部的感觉,而关联的两个类一般是平等的。
  4. 组合,是聚合的一种,区别体现在语义上,聚合的两个类虽然是整体和局部的关系,但是整体消亡了局部仍然存在;而组合则是一荣俱荣,一损俱损。
  5. 继承,指的是某个类是另一个类的父类。

2. 继承

Java 中使用 extends 关键字继承一个已存在且允许继承的类,比如比亚迪汽车继承了汽车:

class Car {
    private int speed;
    
    public Car(int speed) {
        this.speed = speed;
    }
} 

class BYDCar extends Car {
    public BYDCar() {
        super(100);
    }
}

子类会继承父类的一切,包括私有属性,只不过子类不能直接访问父类的私有属性,除非父类有 publicprotected 的方法用于访问。子类还可以添加自己的属性,方法。

在初始化子类时,首先会初始化一个父类对象,因此子类的构造器中的第一行代码必须调用父类的构造器,这通过 super 关键字调用。

如果子类构造器没有显式调用父类构造器,那么默认调用父类无参的构造器。如果父类没有无参构造器,就必须显式调用一个构造器。

super 不仅仅可以调用父类构造器,在其他成员方法中,还可以通过 super 调用父类的方法

2.1 方法重写

子类可以重写父类的方法,叫做方法重写。

方法重写的要求比重载苛刻许多,子类重写的方法的签名需要和父类的方法完全一致,才能称之为重写。一般来说,重写的方法上会添加 @Override 注解。

由于返回值不是方法的签名,因此返回值可以不同,但是仅限于原返回值类型及其子类类型。这叫做有协变的返回类型

方法的重写还不允许子类抛出父类没有抛出的异常,我们在之后还会提到这一点。

static 方法是属于类的,因此不会被重写static 成员根本不参与继承),即使子类定义和父类相同的静态方法,那也只是属于子类的静态方法。

子类重写的方法的可见性不能低于父类.

2.2 阻止继承

使用 final 关键字不仅可以定义常量,还可以作用在方法上或类上,表示不可变方法或类,即禁止该方法被重写或禁止该类被继承。

如果一个方法是 final 方法,且它并不长,则编译器会对它进行内联优化。

2.3 类类型的强制类型转换

类之间的强制类型转换只有一种情况允许:某个类先赋给父类变量,再从父类变量转换回去。

注意,父类不能强制类型转换为子类,虽然可以通过编译,但是运行时会抛出异常。

为了判断能否进行转换,可以使用 instanceof 运算符,主要用于判断一个父类变量是否实际上是一个子类对象。

3. 多态

多态指的是子类对象可以赋给父类变量,父类变量的值可以是任何一个它的子类对象。反过来则不行。

比如:

Car car = new BYDCar();

多态表明了一种替换原则,但是这样做有什么好处呢?要明白这一点,需要明白方法的调用机制。

3.1 理解方法调用

综合多态、继承、重写、重载等内容,总结方法调用的过程:

假设有 x.f(args) 的调用,其中 x 是 C 类的对象

  1. 编译器查看对象的声明类型方法名,列出候选方法(重载)。编译器会一一列举该类及其父类的所有可访问到的该名字的方法。

  2. 编译器确定参数的类型。

  3. 如果是 privatestaticfinal 的方法,或者是构造函数,那么编译器可以准确的知道调用的是哪个方法,这称为静态绑定。

  4. 如果调用的方法依赖对象的实际类型,则称为动态绑定。动态绑定过程最后应该且只能产生一个方法候选者,如果发现多个,编译器将不能决定调用哪一个。

    动态绑定时,JVM 会调用实际对象的类型的方法。假设上面 x 的类型实际上是 C 类的子类 D 类的对象,那么会调用 D 类里的 f 方法(除非没有才会去父类 C 中寻找)。

    字段“重写”

    事实上,字段是不会进行重写的。即使子类有一个和父类一样的字段,在子类的空间中实际上有两个该字段,一个属于父类,一个属于子类。子类的通过 this 引用,而父类的通过 super 引用(如果可见的话)。

    多态中,字段由于不会重写,因此即使子类有一个和父类一样的字段,父类对象总是会访问自己的同名字段(如果可见的话)。

由于每次调用都要经过上述过程,开销比较大,因此 JVM 会提前为每个类生成一张方法表,真正调用方法时,查表即可。

4. 抽象类与抽象方法

抽象的类或者方法需要使用 abstract 修饰,表示必须被继承或必须被实现。

抽象对应面向对象设计中的抽象,它为子类提供一个相当上层的抽象,表示子类都有这样的特性,但是细节不同,所有的子类必须继承或实现这种抽象。

抽象类不允许被实例化,它只能被继承,一般含有 0 个或多个抽象方法。如果含有多个抽象方法,那么子类必须实现每一个抽象方法,除非子类也被声明为抽象的。

抽象方法充当占位的角色,由子类具体实现,因此抽象类中的其他非抽象成员方法可以调用抽象方法。由于动态绑定,而抽象父类又不会被初始化,因此在实际调用方法时一定会调用抽象方法的某一个实现。

抽象类可以有变量,但是它只能指向子类对象(多态)。

抽象类仅仅是不能被实例化,它与其它正常的类没有任何区别。因此它也可以拥有构造方法,只是该构造方法必须由子类调用。

5. 所有类的父类:Object

每个类的最终父类都是 Object,如果一个类没有显式声明它继承自一个类,那么它默认继承自 Object

由于多态,Object 类的变量可以指向任意对象。在 Java 中,只有基本类型(int 等)不是对象,但是它们有对应的属于对象的类型,我们在之后会详细讲述。

Object 类有 9 大方法,这意味着任意一个对象都具有这 9 个基本方法:

方法 说明
protected Object clone() 创建与该对象的类相同的新对象
public boolean equals(Object) 比较两对象是否相等
protected void finalize() 当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法
public Class<?> getClass() 返回一个对象运行时的实例类
public int hashCode() 返回该对象的散列码值
public void notify() 激活等待在该对象的监视器上的一个线程
public void notifyAll() 激活等待在该对象的监视器上的全部线程
public String toString() 返回该对象的字符串表示
public void wait()
public void wait(long timeout)
public void wait(long timeout, int nanos)
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待

这些方法我们在之后会分别遇到,本节我们讲解其中的 equalshashCode 以及 toString

5.1 equals 方法

该方法用于检测一个对象是否等于另一个对象。在 Object 类中,它被实现为检测两个对象引用是否相等。如下面的代码所示:

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

同类之间可以使用 == 进行比较。使用 == 比较就等价于默认的 equals 方法。

建议

无论何时都不要对类类型使用 == 进行比较。不过有一些特殊情况下是可以的。

对于绝大多数类来说,这个方法的默认实现不能满足它们的需求,因此大部分类会选择重写这个方法。比如员工类,只有员工的名字,性别,年龄,身份证一致时,才能认为它们是同一个员工。

一般来说,equals 方法需要满足以下几条性质:

  1. 自反性,对于任何非空引用 x,x.equals(x) 应该为 true。
  2. 对称性,如果 x.equals(y) 为 true,则 y.equals(x) 也应该为 true。
  3. 传递性,如果 x.equals(y) 为 true,y.equals(z) 为 true,则 x.equals(z) 也应该为 true。
  4. 一致性,如果 x 和 y 没有发生变化,那么多次调用 x.equals(y) 结果应该相同。
  5. 对于任意非空引用 x,x.equals(null) 应该为 false。

下面是一个 equals 方法的模板,完全遵守了上面的原则:

public boolean equals(Object otherObject) {
    if (this == otherObject) {
        return true;
    }
    
    if (otherObject == null) {
        return false;
    }
    
    if (getClass() != otherObject.getClass()) {
        return false;
    }
    
    当前类 obj = (当前类) otherObject; // 类型转换
    
    return 当前类的字段比较;
}

讨论

到底是使用 getClass 还是使用 instanceof 进行类型的比较?

getClass 方法是定义在 Object 中的方法,返回该类型的元数据类,每一个类都有一个,且是唯一一个对应的元数据类。

到底使用哪一个完全由类的语义决定,getClass 是十分严格的比较,它严格要求对象的类型必须一致才能通过;而 instanceof 则是可以让一个父类和一个子类进行比较。

Java 提供了工具类 ObjectsArrays 以及 Collections,它们都有对应的 equals 方法进行快速比较。同时,还有一些类拥有 compareTo 等比较方法,使用时需要仔细阅读文档后再调用。

5.2 hashCode 方法

散列码(hash code)是由对象导出的一个整型值,它是没有规律的。

由于它定义在 Object 对象中,因此每个对象都有一个默认的散列码,这个值由对象实际的存储地址导出

有些类重写了该方法,比如 String,它的散列码是由字符串实际内容导出,因此引用了相同字符串的变量的散列码相同。

如果重写了 equals 方法,则必须为可能放入散列表(HashMap 等容器)的对象重写 hashCode 方法,因为这是散列表判断冲突的首要依据。反过来说,如果一个类不需要放入散列表中存储,则 hashCode 毫无作用

hashCodeequals 的关系

如果一个类的对象不会被放进散列容器中存储,则它们毫无关系。

如果会被放进散列容器中存储,则散列容器首先根据 hashCode 进行比较,如果散列码相等,再调用 equals 判断是否真的相等。

在 Java 的散列容器中,Java 要求:如果两个对象相等,则它们的散列码一定相等,反之则不一定。这是因为计算散列码的算法是有可能出现两个对象不相等但是散列码相同的情况的。

因此,我们的最佳实践是在覆盖 equals 方法时应当总是覆盖 hashCode 方法,保证等价的两个对象哈希值也相等

为什么这些容器不直接调用 equals 判断相等呢?首先,散列容器的索引需要通过散列码计算得到,计算得到索引以后就可以把元素直接插入到对应的位置,而无需与其他元素进行多次比较;其次,以 hashCode 判断是否重复的效率比调用 equals 快得多。

那么为什么一些 Java 开发规范要求重写 equals 的同时也要重写 hashCode 方法?首先,你的对象放在散列容器中存储的概率是相当高的;其次,重写的 equals 中根本不会用到 hashCode,这就会出现散列码不同但是两个对象实际上是相同的场景。

重写该方法十分简单,可以对类中的每个字段都调用其 hashCode 方法然后加起来,也可以通过 Objects 提供的 hash 方法,它会为传来的参数(可变参数)一起计算散列码。

5.3 toString 方法

toString 方法为每个对象返回一个字符串表示。

任何类进行字符串的拼接时,都会默认调用其 toString 方法。所以调用 x.toString()x + "" 有相同的效果。详情参见字符串。

Object 类中的实现为:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

这个默认的实现是没有特别意义的,为此,人们一般会选择重写该方法,默认的实现模板为:

@Override
public String toString() {
    return "类名{" +
            "字段1=" + 字段1 +
            ", 字段2='" + 字段2 +
            ", 字段3=" + 字段3 +
            '}';
}

打印的效果就是:类名{字段1=xxx, 字段2=yyy, 字段3=zzz}。也可以使用 [ ] 甚至是 ( ) 包裹。

数组的 toString

数组虽然本质上是对象,但是数组并未重写 toString 方法,你可能会看到下面的令人匪夷所思的输出:[I@1a45e30[I 表示是 int 型的数组,@ 后的内容是 hash code。

补救的办法是使用 Arrays.toString,它接受一个数组,可以在一行中打印出每个数组的元素。

6. 基本类型的包装类

之前提到过,在 Java 中只有八大基本数据类型不是对象。事实上,它们有它们的对象包装版本:

  • int -> Integer
  • byte -> Byte
  • short -> Short
  • long -> Long
  • float -> Float
  • double -> Double
  • char -> Character
  • boolean -> Boolean

除了 intchar,其他的几类都是首字母大写,比较好记。

包装类是不可变的,一旦生成了一个包装类的对象,它其中的值就不会发送改变了。并且包装类本身也是 final 的。

所谓的自动装箱,就是指对每一个需要包装类的地方,会自动为原始类型 Y 的变量 x 调用 Y包装类.valueOf(x),以自动把 x 转换为包装类对象。

自动拆箱则与之相反,当需要原始类型 xxx 时,如果变量是对应的包装类型,则会自动调用其 xxxValue 方法。

比如:

Integer i = 3; // 自动装箱
Integer i = Integer.valueOf(3); // 和上面的代码等价

int j = i; // 自动拆箱
int j = i.intValue(); // 和上面的代码等价

由于自动拆箱的特性,包装类也是可以直接参与数值运算的。

包装类除了自动拆装箱,最便利的是它们提供了很多关于类型转换的方法,比如从字符串转换到 IntegerparseInt 方法。

有了基础类型为什么还需要包装类?

Java 是面向对象语言,包装类让基础类型有了对象的特性,方便用在各种容器中(如 HashMap 数据系相关操作需要用到 hashCode()equals() 方法等等,这些在基础类型中是没有的)。

6.1 缓存机制

由于自动拆箱,可能有人认为下面的代码的答案是 true

Integer i = 1000;
Integer j = 1000;

i == j; // 实际上是 false
i.equals(j); // true

这是由于 i == j 是比较对象的地址,而且这里是比较的包装类对象,因此不会进行自动拆箱。由于这两个对象地址不同,答案自然是 false。正确的比较要通过其 equals 方法。

不过,当 i 和 j 的值为 -128 ~ 127 之间的数时,i == j 会返回 true。比如:

Integer i = 127;
Integer j = 127;
System.out.println(i == j);

Integer ii = 128;
Integer jj = 128;
System.out.println(ii == jj);

答案是先打印 true 再打印 false

这是由于 -128 ~ 127(Byte 的取值范围)之间的数在 JVM 中被放到了常量池中,因此只存在一个 127 的对象。无论是 Byte,还是 IntegerShortLong,只要你的值处于 Byte 的取值范围中,那么它们总是同一个对象。

Integer 为例,其内部持有一个名为 IntegerCache 的整数缓存,如果 Integer.valueOf 中的值在 -128 ~ 127 之间,则直接从缓存中(常量池)拿对象。如下面的代码所示:

static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}

包装类基本上都有缓存机制:

  1. Boolean 取值范围内的所有值都会被缓存,也就是 truefalse
  2. Byte 取值范围内的所有值都会被缓存。
  3. ShortInteger 缓存 -128 ~ 127 直接的数。
  4. Character 缓存 '\u0000' ~ '\u007F'

提示

Integer 的缓冲池 IntegerCache 是比较特殊的,这个缓冲池的下界是 -128,上界默认是 127,但是这个上界是可调的,在启动 JVM 的时候,通过
-XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。

7. String

Java 中的字符串不是原始类型,而是用一个名为 String 的类来表示的。

Java 的字符串字面量使用 "" 括起来,如果 "" 里没有任何东西就叫做空串,但是并非为 null

字符串字面量和其他字面量不一样,每一个字符串字面量都是一个对象,你可以直接对字符串字面量调用方法,比如 "123".repeat(3) 会得到 "123123123" 这个新字符串。

String 类是不可变的,它内部使用 private final 关键字修饰保存字符串的数据结构,同时它本身也是 final 的,这意味着不能通过继承破坏这个不可变性。同时,String 没有提供任何可以修改字符串中某个字符的方法,除非对字符串取子串然后进行拼接(但是所谓的取子串和拼接新字符串也只是创建新的对象)。这意味着 "Hello" 字符串的内容永远是这五个字符,无论发生什么都不会改变。

注意

给字符串变量赋新值实际上只是改变了引用,字符串本身没有改变。

不可变的好处在于:

  1. 可以缓存 hash 值。

    因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

  2. String Pool(字符串常量池)的需要。

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

    字符串常量池就是为了重复利用字符串,节省内存空间。

  3. 安全性。

    String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

  4. 线程安全性。

    String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

Java 语言为字符串重载了运算符 +,可以使用 + 拼接字符串;当字符串与非字符串拼接时,会将其转换为字符串:基本类型会调用 String.valueOf() 静态方法,而类类型则会调用其 toString 方法。

String 底层是 char[],但是在 Java 9 中,改用 byte[] 实现了,同时使用了一个 coder 变量记录字符串的编码,这是为了压缩字符串,以节省空间

检测两个字符串是否相等,需要使用 equals 方法,但是千万不要使用 ==

下表列出了 String 类的常用 API,这些 API 大多都是很常用的:

方法 作用
new String(String str) 根据 str 构造一个字符串,即构造一个 str 的副本。
new String(char[] value) 根据字符数组构造字符串。
new String(byte[] bytes) 根据字节数组构造字符串。
new String(StringBuffer buffer) 根据 StringBuffer 构造字符串。
new String(StringBuilder builder) 根据 StringBuilder 构造字符串。
int length() 得到一个字符串的长度。
boolean isEmpty() 判断是否为空串。
char charAt(int index) 返回 index 上的字符。
void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) 把字符串中指定范围内的字符复制到字符数组中。
byte[] getBytes() 获得字符串的 byte[] 数组表示。
boolean equals(Object anObject) 将此字符串与指定对象进行比较。 当且仅当参数不为 null 并且是表示与此对象相同的字符序列的 String 对象时,结果才为 true。
boolean equalsIgnoreCase(String anotherString) 将此 String 与另一个 String 进行比较,忽略大小写。
int compareTo(String anotherString) 按字典顺序比较两个字符串。如果此 String 更大,返回小于 0 的数。
int compareToIgnoreCase(String str) 上面方法的忽略大小写版本。
boolean startsWith(String prefix) 判断字符串是否以 prefix 开头。
boolean endsWith(String suffix) 判断字符串是否以 suffix 结尾。
int indexOf(String str) 返回字串在字符串中的索引,如有多个匹配,返回第一个。
int lastIndexOf(String str) 返回字串在字符串中的索引,如有多个匹配,返回最后一个。
String substring(int beginIndex, int endIndex) 截取字串。包括开头,不包括结尾。
boolean matches(String regex) 判断该字符串是否与正则表达式匹配。
boolean contains(CharSequence s) 判断该字符串是否包含另一个字符串。
String replaceFirst(String regex, String replacement) 用给定的字符串替换掉此字符串中与给定正则表达式匹配的第一个子字符串。
String replaceAll(String regex, String replacement) 用给定的字符串替换掉此字符串中与给定正则表达式匹配的全部子字符串。
String replace(CharSequence target, CharSequence replacement) 用给定的字符串 replacement 替换掉此字符串中和 target 相同的全部子字符串。
String[] split(String regex) 根据给定正则表达式的匹配拆分此字符串。
String join(CharSequence delimiter, CharSequence... elements) 使用 delimiter 连接 elements,返回拼接后的字符串
String toLowerCase() 字符串转小写
String toUpperCase() 字符串转大写
String trim() 去除字符串的前后空格
String strip() 去除字符串的前后空格(支持删除 Unicode 空格)
boolean isBlank() 判断字符串是否为空串或仅包含空格类的字符。
char[] toCharArray() 获得字符串的 char[] 数组表示。
String format(String format, Object... args) 格式化字符串
String valueOf(Object obj) 将任意对象转换为字符串
String valueOf(基本类型 obj) 将任意基本类型转换为字符串
String repeat(int count) 将字符串重复 count 次后返回

7.1 String Pool(字符串常量池)

字符串常量池(String Pool)保存着所有字符串字面量,这些字面量在编译时期就确定。

什么叫编译时期就已经确定?

比如有下面这段程序:

String a = "123";

String b = a.repeat(3);

程序在编译时,就已经确定 a 的值是 "123",这是显而易见的,因此,Java 直接把它放到字符串常量池中,之后的每一个 "123" 字面量都是同一个对象。

而 b 这个字符串,显然我们要等到它执行时才知道 b 是什么结果,因此 b 就是运行时创建的字符串,放在堆中。

我们可以用下面这个程序证明这一点:

String a = "123"; // 常量池
String b = "123"; // 常量池

String c = a.repeat(3); // 堆
String d = "123123123"; // 常量池

// == 比较两个对象的地址,所以利用 == 判断它们是否为同一对象
System.out.println(a == b); // 输出 true
System.out.println(c == d); // 输出 false

不仅如此,我们还可以使用 Stringintern() 方法在运行过程将字符串添加到 String Pool 中:当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(这会使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得同一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。

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

特别说明一下,String(String) 的构造函数是根据字面值在堆上创建一个对象,并且值指向参数中的字符串。而这里给的参数是字面量,意味着编译时就能确定它,所以这个字面量放入了常量池,s1 这个对象在堆上,并且指向字符串常量池中的常量。

Java 的设计者认为共享字符串带来的收益远远高于可变字符串带来的不能共享,这也是字符串不可变的原因之一。

7.2 StringBuilderStringBuffer

StringBuilderStringBuffer用于改变字符串的类,其中,StringBuilder 是非线程安全的,StringBuffer 是线程安全的。

我们知道,String 不可变是因为底层的数组是 final 的,而且也不提供对外修改的接口;但是这两个类不一样,它们的底层也是数组,但是它们的数组并没有使用 final 修饰,同时也提供了大量的修改字符串的方法。

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

StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据:适用 String
  2. 单线程操作字符串缓冲区下操作大量数据:适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据:适用 StringBuffer

7.2.1 字符串拼接的原理

我们提到过,Java 为了拼接字符串的方便,给 String 类重载了 ++= 运算符,这也是 Java 中仅有的两个重载过的元素符。

String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;

那么,Java 是怎么实现拼接的呢?在上面的例子中,本质是使用 StringBuilderStringBuffer

我们把上面的例子编译成字节码,结果为:

image

我们从字节码中看到,三个 + 最后就转换为了 StringBuilderappend 方法。

不过,在循环内使用 + 进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象

实际上,Java 会对字符串拼接做一些优化:

  • 如果拼接的都是字符串字面量,比如 "A" + "B" 这样的拼接,则在编译时编译器会将其直接优化为一个完整的字符串,和 "AB" 没有任何区别。
  • 如果包含变量,比如上面的例子,则会使用 StringBuilder

String 还有一个 concat 方法,它用于拼接两个字符串,它的实现原理是预先设置一个足够大的字符数组,然后把两个字符串的字符一一放入。

如果你循环调用 concat 方法拼接多个字符串,实际上效率是不如 StringBuilder 的,这是因为 StringBuilder 设置有字符串缓冲区,大量字符串的操作都是操作缓存区,只要缓存区足够大,它就不用创建新缓冲区并移动字符,相比于 concat 方法减少了创建数组的消耗。

7.3 字符串格式化

字符串格式化指的是,在字符串中使用占位符规定某个数据的格式,然后利用格式化方法把数据插入到字符串中,这个数据被插入到字符串时会根据占位符规定好的格式来进行格式的转换。

看一个例子:

String.format("你好,%s,明年你 %d 岁了", name, age);

占位符就是以 % 开头的字符串,也叫格式制符。

下表列出了常用的格式制符:

格式制符 说明
%d 十进制整数
%x 十六进制整数
%o 八进制整数
%f 定点浮点数
%s 字符串
%c 字符
%b 布尔
%h 散列码
%e 指数形式的浮点数
%g 通用浮点数,会在 %e 和 %f 中选择最短的那个
%a 十六进制浮点数
%n 平台相关的分隔符

基本的格式制符告诉格式化器要把该数据展示成何种形式,还有具体对格式的规定符号,如下表所示:

符号 作用
+ 打印正数和负数的符号
这是一个单独的空格,表示要在正数之前添加空格
0 表示在数字前补 0
- 左对齐
( 表示将负数括在括号里,会去掉负号
#(和 %f 结合) 包含小数点
#(和 %x %o 结合) 添加前缀 0x 或 0
n$ 参数索引,表示使用后面参数中的第 n 个数据作为这个地方的数据
< 使用前一个格式制符的原始数据

看几个例子:

System.out.println(String.format("%d \t %<#x", 1233));
System.out.println(String.format("%2$(,f \t %1$+d", 123, -1334f));

结果:
image

合理利用字符串格式化方法可以极大的减轻字符串拼接的工作量。

标签:String,对象,子类,面向对象,字符串,父类,方法
From: https://www.cnblogs.com/fahax1k1/p/16850548.html

相关文章

  • c++从入门到精通——面向对象初探以及友元函数、对象
    面向对象每个对象内存地址独一无二,空对象分配一个字节空间#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespacestd;classPerson{public://intm_A;voi......
  • C++面向对象高级开发(六)写好一个String类
    类的内部:public:构造函数、拷贝构造、拷贝赋值、析构函数的接口和辅助函数以及它的实现private:参数  类的外部:内联:inline构造函数:判断是否有初值:有初值:分配......
  • 面向对象编程
    1.对象Object对象的两个部分:属性、行为面向对象编程的三大特点:封装、继承、多态2.封装写程序的时候也可以采用封装的理念,对于一些内容我们不提供接口来使用它们,它们属......
  • C语言面向对象思想
     (17条消息)C语言面向对象思想_lzs_blog的博客-CSDN博客_c实现面向对象C语言面向过程的,而C++是面向对象的。l 面向过程,我认为过程就是步骤,是解决问题的按部就班。l......
  • Python学习五:面向对象设计程序
    文章目录​​一、引言​​​​二、对象​​​​定义​​​​三、类​​​​定义​​​​四、面向对象程序的设计特点​​​​三大基本特点:封装、继承、多态​​​​1.封装​......
  • 面向对象程序设计
    一、创建大雁类并定义飞行方法   二、通过类属性统计类的实例个数   三、在模拟电影点播功能时应用属性    四、创建水果基类及其派生类  ......
  • java面向对象-->封装
    封装封装的作用是在于:如何正确设计对象的属性和方法。封装的重要原则:对象代表什么,就要封装对应的数据,并提供数据对应的行为。比如说人画圆,涉及到了人和圆俩个对象,画圆......
  • 软考中级(软件设计师)——面向对象技术(上午12分)(下午30分)(超重点)
    软考中级(软件设计师)——面向对象技术(上午12分)(重点)目录​​软考中级(软件设计师)——面向对象技术(上午12分)(重点)​​​​面向对象的基本概念(★★★★★)​​​​面......
  • 面向对象基础回顾
    基本知识回顾:类与对象:成员变量,方法,构造器(初始化类的对象)内部块,代码块一个JAVA文件可以定义多个类,但是只有一个类用PUBLIC修饰,且该修饰的类名必须为JAVA代码文件名称 ......
  • 实验二 面向对象程序设计
    一、实验目的1.掌握类的声明、对象的创建。2.掌握方法的定义和调用、方法的重载。3.掌握构造函数的使用。4.掌握类的继承、掌握隐藏与重写(覆盖)。5.掌握抽象类与接口。二、实......