Java编程中是不是可以继承String类?并说明原因。

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java编程中是不是可以继承String类?并说明原因。相关的知识,希望对你有一定的参考价值。

String表示字符串,Java中所有字符串的字面值都是String类的实例,例如“ABC”。字符串是常量,在定义之后不能被改变,字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享它们。例如:

String str = "abc";

相当于

char data[] = 'a', 'b', 'c';
String str = new String(data);

这里还有一些其他使用字符串的例子:

System.out.println("abc");
String cde = "cde";
System.out.println("abc" + cde);
String c = "abc".substring(2,3);
String d = cde.substring(1, 2);

String类提供了检查字符序列中单个字符的方法,比如有比较字符串,搜索字符串,提取子字符串,创建一个字符串的副本、字符串的大小写转换等。实例映射是基于Character类中指定的Unicode标准的。 Java语言提供了对字符串连接运算符的特别支持(+),该符号也可用于将其他类型转换成字符串。字符串的连接实际上是通过StringBuffer或者StringBuilder的append()方法来实现的,字符串的转换通过toString方法实现,该方法由 Object 类定义,并可被 Java 中的所有类继承。 除非另有说明,传递一个空参数在这类构造函数或方法会导致NullPointerException异常被抛出。String表示一个字符串通过UTF-16(unicode)格式,补充字符通过代理对(参见Character类的 Unicode Character Representations 获取更多的信息)表示。索引值参考字符编码单元,所以补充字符在String中占两个位置。
定义 属性 构造方法
使用字符数组、字符串构造一个String
使用字节数组构造一个String
使用StringBuffer和StringBuider构造一个String
一个特殊的保护类型的构造方法
其他方法
getBytes
比较方法
hashCode
substring
replaceFirst、replaceAll、replace区别
copyValueOf 和 valueOf
intern
String对“+”的重载
String.valueOf和Integer.toString的区别
参考资料
一、定义
public final class String implements java.io.Serializable, Comparable<String>, CharSequence

从该类的声明中可以看出String是final类型的,表示该类不能被继承,同时该类实现了三个接口:java.io.Serializable、 Comparable<String>、 CharSequence

二、属性

private final char value[];

这是一个字符数组,并且是final类型,他用于存储字符串内容,从fianl这个关键字中可以看出,String的内容一旦被初始化了是不能被更改的。 虽然有这样的例子: String s = “a”; s = “b” 但是,这并不是对s的修改,而是重新指向了新的字符串, 从这里能知道,String其实就是用char[]实现的。

缓存字符串的hash Code,默认值为 0

因为String实现了Serializable接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
三、构造方法
String类作为一个java.lang包中比较常用的类,自然有很多重载的构造方法.在这里介绍几种典型的构造方法:
1.使用字符数组、字符串构造一个String
String就是使用字符数组(char[])实现的。所以可以使用一个字符数组来创建一个String,那么这里值得注意的是,当使用字符数组创建String的时候,会用到Arrays.copyOf方法和Arrays.copyOfRange方法。这两个方法是将原有的字符数组中的内容逐一的复制到String中的字符数组中。同样,也可以用一个String类型的对象来初始化一个String。这里将直接将源String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后是不可以改变的,所以也就不用担心改变源String的值会影响到目标String的值。
当然,在使用字符数组来创建一个新的String对象的时候,不仅可以使用整个字符数组,也可以使用字符数组的一部分,只要多传入两个参数int offset和int count就可以了。
2.使用字节数组构造一个String
在Java中,String实例中保存有一个char[]字符数组,char[]字符数组是以unicode码来存储的,String 和 char 为内存形式,byte是网络传输或存储的序列化形式。所以在很多传输和存储的过程中需要将byte[]数组和String进行相互转化。所以,String提供了一系列重载的构造方法来将一个字符数组转化成String,提到byte[]和String之间的相互转换就不得不关注编码问题。String(byte[] bytes, Charset charset)是指通过charset来解码指定的byte数组,将其解码成unicode的char[]数组,构造成新的String。
这里的bytes字节流是使用charset进行编码的,想要将他转换成unicode的char[]数组,而又保证不出现乱码,那就要指定其解码方式
同样使用字节数组来构造String也有很多种形式,按照是否指定解码方式分的话可以分为两种:

如果在使用byte[]构造String的时候,使用的是下面这四种构造方法(带有charsetName或者charset参数)的一种的话,那么就会使用StringCoding.decode方法进行解码,使用的解码的字符集就是指定的charsetName或者charset。在使用byte[]构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作。主要体现代码如下:

static char[] decode(byte[] ba, int off, int len)
String csn = Charset.defaultCharset().name();
try
// use charset name decode() variant which provides caching.
return decode(csn, ba, off, len);
catch (UnsupportedEncodingException x)
warnUnsupportedCharset(csn);

try
return decode("ISO-8859-1", ba, off, len);
catch (UnsupportedEncodingException x)
// If this code is hit during VM initialization, MessageUtils is
// the only way we will be able to get any kind of error message.
MessageUtils.err("ISO-8859-1 charset not available: "
+ x.toString());
// If we can not find ISO-8859-1 (a required encoding) then things
// are seriously wrong with the installation.
System.exit(1);
return null;


3.使用StringBuffer和StringBuider构造一个String

作为String的两个“兄弟”,StringBuffer和StringBuider也可以被当做构造String的参数。

public String(StringBuffer buffer)
synchronized(buffer)
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());


public String(StringBuilder builder)

this.value = Arrays.copyOf(builder.getValue(), builder.length());

当然,这两个构造方法是很少用到的,至少我从来没有使用过,因为有了StringBuffer或者StringBuilfer对象之后可以直接使用他们的toString方法来得到String。关于效率问题,Java的官方文档有提到说使用StringBuilder的toString方法会更快一些,原因是StringBuffer的toString方法是synchronized的,在牺牲了效率的情况下保证了线程安全。

public String toString()

// Create a copy, don't share the array
return new String(value, 0, count);

this.value = Arrays.copyOfRange(value, offset, offset+count);

4.一个特殊的保护类型的构造方法

String除了提供了很多公有的供程序员使用的构造方法以外,还提供了一个保护类型的构造方法(Java 7):
String(char[] value, boolean share)

// assert share : "unshared not supported";
this.value = value;

从代码中看出,该方法和 String(char[] value)有两点区别,第一个,该方法多了一个参数: boolean share,其实这个参数在方法体中根本没被使用,也给了注释,目前不支持使用false,只使用true。那么可以断定,加入这个share的只是为了区分于String(char[] value)方法,不加这个参数就没办法定义这个函数,只有参数不能才能进行重载。那么,第二个区别就是具体的方法实现不同。前面提到过,String(char[] value)方法在创建String的时候会用到Arrays的copyOf方法将value中的内容逐一复制到String当中,而这个String(char[] value, boolean share)方法则是直接将value的引用赋值给String的value。那么也就是说,这个方法构造出来的String和参数传过来的char[] value共享同一个数组。 那么,为什么Java会提供这样一个方法呢? 首先分析一下使用该构造函数的好处:

首先,性能好,这个很简单,一个是直接给数组赋值(相当于直接将String的value的指针指向char[]数组),一个是逐一拷贝。当然是直接赋值快了。
其次,共享内部数组节约内存但是,该方法之所以设置为protected,是因为一旦该方法设置为公有,在外面可以访问的话,那就破坏了字符串的不可变性。例如如下YY情形:
char[] arr = new char[] 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd';

String s = new String(0, arr.length, arr); // "hello world"
arr[0] = 'a'; // replace the first character with 'a'
System.out.println(s); // aello world
如果构造方法没有对arr进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对arr的修改就相当于修改了字符串。

所以,从安全性角度考虑,他也是安全的。对于调用他的方法来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。
在Java 7 之前有很多String里面的方法都使用这种“性能好的、节约内存的、安全”的构造函数。比如:substring、replace、concat、valueOf等方法(实际上他们使用的是public String(char[], int, int)方法,原理和本方法相同,已经被本方法取代)。
但是在Java 7中,substring已经不再使用这种“优秀”的方法了,为什么呢? 虽然这种方法有很多优点,但是他有一个致命的缺点,对于sun公司的程序员来说是一个零容忍的bug,那就是他很有可能造成内存泄露。 看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。下面是示例代码。
String aLongString = "...a very long string...";

String aPart = data.substring(20, 40);
return aPart;
在这里aLongString只是临时的,真正有用的是aPart,其长度只有20个字符,但是它的内部数组却是从aLongString那里共享的,因此虽然aLongString本身可以被回收,但它的内部数组却不能。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。

新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的substring比原来的更健壮。

substring方法已经为了其鲁棒性放弃使用这种share数组的方法,但是这种share数组的方法还是有一些其他方法在使用的,这种方式构造对应有很多好处,其次呢,其他的方法不会将数组长度变短,也就不会有前面说的那种内存泄露的情况(内存泄露是指不用的内存没有办法被释放,比如说concat方法和replace方法,他们不会导致元数组中有大量空间不被使用,因为他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短!)。
四、其他方法
length() 返回字符串长度
isEmpty() 返回字符串是否为空
charAt(int index) 返回字符串中第(index+1)个字符
char[] toCharArray() 转化成字符数组
trim() 去掉两端空格
toUpperCase() 转化为大写
toLowerCase() 转化为小写
String concat(String str) //拼接字符串
String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar符
//以上两个方法都使用了String(char[] value, boolean share);
boolean matches(String regex) //判断字符串是否匹配给定的regex正则表达式
boolean contains(CharSequence s) //判断字符串是否包含字符序列s
String[] split(String regex, int limit) 按照字符regex将字符串分成limit份。
String[] split(String regex)

String string = "h,o,l,l,i,s,c,h,u,a,n,g";
String[] splitAll = string.split(",");
String[] splitFive = string.split(",",5);
splitAll = [h, o, l, l, i, s, c, h, u, a, n, g]
splitFive = [h, o, l, l, i,s,c,h,u,a,n,g]
参考技术A 不可以,因为String是final类型,不可以继承,不可以修改 参考技术B 不能,String是不能被继承的

Java多态的学习

首先,我要说明的是,继承、封装、多态并不是针对JAVA,c#或者其他某种语言产生的,它是面向对象思想下产生的一个概念。

让我自己说的话,我只能用三句话来描述(不知道对不对,请高手指点):

  * 继承:使得子类继承父类的属性和方法,也可以使用父类的功能。

  * 封装:将具体实现隐藏,只留给用户使用的接口。

  * 多态:相似类型在使用同一基类方法时,可以表现出与基类不同的行为。

一直记不住这些概念,百度了下,放在这供参考:

1、继承(inheritance)     继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。

继承是为了重用父类代码,同时为实现多态性作准备。
  2、封装(encapsulation)     类使得数据和对数据的操作集成在一起,从而对使用该类的其他人来说,可以不管它的实现方法,而只管用它的功能,从而实现所谓的信息隐藏。  封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据。

3、多态(polymorphism)

方法的重写、重载与动态连接构成多态性。Java之所以引入多态的概念,原因之一是它在类的继承问题上和C++不同,后者允许多继承,这确实给其带来的非常强大的功能,但是复杂的继承关系也给C++开发者带来了更大的麻烦,为了规避风险,Java只允许单继承,派生类与基类间有IS-A的关系(即“猫”is a “动物”)。这样做虽然保证了继承关系的简单明了,但是势必在功能上有很大的限制,所以,Java引入了多态性的概念以弥补这点的不足,此外,抽象类和接口也是解决单继承规定限制的重要手段。同时,多态也是面向对象编程的精髓所在。     多态又分为设计时多态和运行时多态,例如重载又被称为设计时多态,而对于覆盖或继承的方法,JAVA运行时系统根据调用该方法的实例的类型来决定选择调用哪个方法则被称为运行时多态。总而言之,面向对象的设计的典型特点就是继承,封装和多态,这些特点也是面向对象之所以能如此盛行的关键所在。

对于多态,可以总结它为:

    一、使用父类类型的引用指向子类的对象;该引用只能调用父类中定义的方法和变量;

    二、如果子类中重写了父类中的一个方法,那么在调用这个方法的时候,将会调用子类中的这个方法;(动态连接、动态调用)

    三、变量不能被重写(覆盖),重写的概念只针对方法。

对多态的理解:

个人觉得多态是最为抽象的一个概念,那就用实例来解释了。

1. public, protected的方法具有多态性,但是如果某个方法是静态的,就不具有多态性,因为静态方法是与类而非单个对象相关联的。

技术分享
package com.wx.test;

public class Base {

    public void publicMethod() {
        System.out.println("Base public Method");
    }

    protected void protectedMethod() {
        System.out.println("Base protected Method");
    }

    public static void publicStaticMethod() {
        System.out.println("Base protected Method");
    }
}

public class Sub1 extends Base {

    public void publicMethod() {
        System.out.println("Sub1 public Method");
    }

    protected void protectedMethod() {
        System.out.println("Sub1 protected Method");
    }

    public static void publicStaticMethod() {
        System.out.println("Sub1 protected Method");
    }
}

public class Sub2 extends Base {

    public void publicMethod() {
        System.out.println("Sub2 public Method");
    }

    protected void protectedMethod() {
        System.out.println("Sub2 protected Method");
    }

    public static void publicStaticMethod() {
        System.out.println("Sub2 protected Method");
    }
}

public class Test {
    public static void main(String[] args) {
        Base base1 = new Sub1();
        Base base2 = new Sub2();
        base1.publicMethod();
        base2.publicMethod();
        base1.protectedMethod();
        base2.protectedMethod();
        base1.publicStaticMethod();
        base2.publicStaticMethod();
    }
}
View Code

     运行结果如下:

技术分享
Sub1 public Method
Sub2 public Method
Sub1 protected Method
Sub2 protected Method
Base protected Method
Base protected Method
View Code

   从以上结果可以看到,将子类对象转换成父类对象,对于public和protected方法调用时,是调用了实际创建的子类方法。问题就在于编译器只有一个父类对象,它无法知道调用哪个方法才对。这个问题解决办法就是后期绑定,就是编译器确实不知道调用哪个方法,运行时才根据对象类型进行绑定,从而调用子类方法。也就是编译器一直不知道对象类型,方法调用机制可以知道类型信息,从而找到正确的方法体。

      2.“覆盖”私有方法

技术分享
package com.wx.test;

public class Super {
    
    private void function()
    {
        System.out.println("Super private Method");
    }
    public static void main(String[] args) {
        Super sup = new Sub(); 
        sup.function();
    }

}

public class Sub extends Super{
    public void function()
    {
        System.out.println("Sub public Method");
    }

}
View Code

      运行结果如下:

技术分享
Super private Method
View Code

     我们期望输出Sub public Method,但是由于private方法被自动认为是final方法而且对于子类是不可见的,因此Sub中的function()是一个新的方法。所以私有方法是不能被覆盖的。

  3. 属性不可以是多态的

技术分享
package com.wx.test;

public class Super {
    public int field = 0;
    public int getField() { return field; }
}

public class Sub extends Super{
    public int field = 1;
    public int getField() { return field; }

}

public class Test {
    public static void main(String[] args) {
        Super sup = new Sub();
        Sub sub  = new Sub();
        System.out.println("sup.field = "+ sup.field +"," + "sup.getField() = " + sup.getField());
        System.out.println("sub.field = "+ sub.field +"," + "sub.getField() = " + sub.getField());
    }

}
View Code

     运行结果如下:

技术分享
sup.field = 0,sup.getField() = 1
sub.field = 1,sub.getField() = 1
View Code

   不过在实践中这种情况一般不会发生,一是属性会被定义为private,因此不能直接访问,二是子类不会定义和基类相同名字的属性,这种做法容易混淆。

 

以上是关于Java编程中是不是可以继承String类?并说明原因。的主要内容,如果未能解决你的问题,请参考以下文章

java.lang里面的类都有哪些可以被继承?

java编程思想-字符串

Java多态的学习

JAVA学习笔记--组合与继承

举例说明java面向对象的封装继承多态

Java自用高级基础编程-2.Java常用类