我们前面已经介绍了java的基础知识点以及简单的OOP面向对象编程思想。也就意味着你已经开始走入了java的世界里。但目前的你,就仿佛一个门外汉,刚刚才把脚跨进这个宝库里,还没来得及去发现这个宝库里面的珍宝呢。不过,也不用担心,成功总是留给有准备的人滴,而我们已经为你做好了这些准备,接下来,就让我们一起去深入了解java这个宝库,去发现更多有趣的东西吧。就好比我们今天的主题:
我们在java基础篇里面就曾介绍过String的知识,但也仅局限于表面性的东西(基础阶段嘛),到了现在,我们将再给你掀开它的一层面罩,让你对其有个更深入的了解。话不多说,精彩马上开始!
首先,我们来看看String类是个什么东西:它的源码(部分)如下:
public final class String implements Serializable, Comparable<String>, CharSequence {
...
private final char[] value;
private final int offset;
private final int count;
...
/**
* Creates an empty string.
*/
public String() {
value = EmptyArray.CHAR;
offset = 0;
count = 0;
}
/*
* Private constructor used for JIT optimization.
*/
@SuppressWarnings("unused")
private String(String s, char c) {
offset = 0;
value = new char[s.count + 1];
count = s.count + 1;
System.arraycopy(s.value, s.offset, value, 0, s.count);
value[s.count] = c;
}
....
}
你看,我们从源码中可以发现,String类是被 final 关键字所修饰的,也即是说,String是不能被继承的,什么意思呢?我们知道,继承能够实现多态,有利于代码的复用。但有的时候,有一些原则性的东西,就好比一辆汽车的核心原件,我们是不能随意去做变动的。因为一但做了改动,或许整辆车都将报废。而在java语言中,为了保证系统能够正确运行,工程师们将一些系统级的类用final进行修饰,以免它们被人随意修改而造成一些不必要的麻烦。另外,用心的孩子还会发现在源码里,String对象实际上也是由一组final修饰的char[]所构成,因此,String对象又被称为字符串常量,它是不能被再次修改并且不可被继承的。这点可以给java基础-字符串String这篇博文作证,而关于String的内容,在这篇文章里面都作了相关的介绍。这里便不再多数,我们说说另外两位主角:StringBuilderr和StringBuffer。
先看StringBuilder:
public final class StringBuilder extends AbstractStringBuilder implements
Appendable, CharSequence, Serializable {
······
public StringBuilder() {
}
public StringBuilder(int capacity) {
super(capacity);
}
public StringBuilder(CharSequence seq) {
super(seq.toString());
}
public StringBuilder(String str) {
super(str);
}
······
我们发现,在这里,StringBuilder继承了一个AbstractStringBuilder,并且它的构造方法最终也全部都由父类的构造方法实现,那么,我们再来看看它的父类是怎么构造出一个StringBuilder对象的:
abstract class AbstractStringBuilder {
static final int INITIAL_CAPACITY = 16;
private char[] value;
private int count;
···
AbstractStringBuilder() {
value = new char[INITIAL_CAPACITY];
}
AbstractStringBuilder(int capacity) {
if (capacity < 0) {
throw new NegativeArraySizeException(Integer.toString(capacity));
}
value = new char[capacity];
}
AbstractStringBuilder(String string) {
count = string.length();
shared = false;
value = new char[count + INITIAL_CAPACITY];
string._getChars(0, count, value, 0);
}
·····
}
我们再看,发现在这个抽象父类里面,value值不再final修饰的char[]了,也就是说,我们的StringBuilder是可以被修改的。如果说String是字符串常量,那么它就是一个字符串变量。那么,StringBuffer又是怎样的呢?我们再看它的源码:
public final class StringBuffer extends AbstractStringBuilder implements
Appendable, Serializable, CharSequence {
····
}
看到这里,我们便已经发现了一个问题,StringBuilder和StringBuffer都是继承自AbstractStringBuilder。也就是说,两者的性质其实是一样的,都属于字符串变量。那么,它们两者之间有什么不同呢?其实是有的,但目前不宜多说,因为涉及到了以后的内容,这里先挖坑。只需知道StringBuilder是线程不安全,而StringBuffer是线程安全的即可。这个知识点是常见面试题之一,所以需要掌握。
接下来,我们继续说StringBuider和StringBuffer。但由于这两者的其他操作是一致的,就是线程安不安全的问题而已,所以这里就直接选了StringBuffer作后面的介绍,等同于StringBuilder。
有印象的同学会知道,我们在基础篇的时候说到,要改变String的长度,可以用字符串连接符将字符串及其周围的其他数据拼接成一个新的字符串。这是因为String对象一旦确立,长度是不能改变的。而现在,StringBuffer作为一个变量存在,那么它是可以改变长度,自然也可以改变增加字符串的呀。这个怎么操作的,其实使用的就是:
public StringBuffer append()
这个方法,通过这个方法,可以实现将StringBuffer对象和字符char/字符串/另一个StringBuffer对象/对象拼接起来,而不需再创建一个StringBuffer来存放。而这个方法是与String有所不同的,其他的方法和String都是一致的,这里便同样不做介绍。忘了的童鞋可以回翻基础篇的博文去看一下。
对于Object类,这是个有故事的类。我们知道,java的继承机制是单继承的,也就是一个类只能继承一个父类,那么,这个父类又可以继承另一个父类,如此反复下去,岂不是陷入了一个死循环?所以,为了避免这种死循环,我们把所有的类的特性重新封装成一个新的父类,作为所有类的父类,并且默认如果一个类没有继承其他的父类,则该类直接继承自这个父类。没错,他就是Object类,是所有类的父亲。对于这个类,目前不建议去看源码,因为里面涉及很多的JNI(java native interface)。这对于目前的目录安排而言,属于超纲部分了,在以后介绍JNI时再来说这个。目前我们需要了解的是这个类的多态性应用及一些常用的方法:
先说多态性应用,我们在介绍多态的时候,就已经说了,多态是指对象的多种形态,类的多态性就包括从向上转型和向下转型两种。从这个层面而言,所有的子类类型都可以向上转型为Object,这就使得当要进行某种集中性操作时,可以避免大量的代码冗余。一个例子来表示:在Android开发中,有这么一个类Log,是用来打印某种调试日志用的。但它只能打印String类型的数据,所以当我们要打印其他类型的数据时,都需要先进行类型转换。为了集中化处理,我们做了一下措施:
//集中进行数据打印的工具类
public class LogUtil{
//静态方法,可供外部类直接调用
//第一个参数为标签,用来标志打印的数据的意义
//第二个参数为任意类型的信息
public static void showLog(String Tag, ObJect obj);
//Log类是Android特有的一个类,用来打印某种调试日志
//i是Log类的一个静态方法,是info的缩写
//两个参数都需要为String类型
Log.i(tag,String.valueOf(obj));
}
你看,在这里,我们对这个Log类进行了封装,在往后需要打印什么日志的时候,就直接调用showLog方法,并且提供参数,那么这个类会将我们提供的任何类型的参数转换成String类型。在这其中,Object类就功不可没了,因为它可以代表所有的子类类型。也就在这里实现了多态。当然,这个例子是在Android开发中使用到的,也是以后比较常用的一个方法,这里做个简单的介绍。让大家先有个初步认识。主要的目的还是放在Object多态性的理解上。
好了,接下来就是关于Object的几个重要方法:
1.toString()
这个方法的作用是获得指定对象的字符串表示。什么意思呢?也就是说,当我们创建出一个对象的时候,那么这个对象会存在在堆空间中,并且虚拟机生成出一个内存地址,这个内存地址可以用一个hashcode来表示。但是问题在于,我们是不能看见这个对象的,那么它是不是这样的呢?我们可以通过这个方法来获得,它的返回表示为”类型名称@内存地址”。比如:
当然,这个方法是可以在子类中重写的,就像String类,它重写了toString方法,返回的是当前的对象名称。比如:
2.equals(Object obj)
关于这个方法,是比较重要,也是必要让人混淆的。它的作用是比较当前对象与obj对象是否为同一个对象。什么意思呢?我们在上面已经说了,对象是存储在堆空间,并且用一个hachcode来指示内存地址的。也就是说,当我们需要调用这个对象时,就需要取到这个内存地址,可以说,这个内存地址,就是对象的唯一标识码,如果hashcode不相等了,那么就肯定说明这两个对象不相等。那么是不是说,如果hashcode相等了,那么这两个对象就肯定相等了呢?这也未必。至于原因,会在将第三个方法的时候进行解释。现在先挖坑。我们先说如何重写这个equals方法,主要有五点要求:
1.自反性:对于任何非空引用x,x.equals(x)应该返回true。
2.对称性:对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。
3.传递性:对于任何引用x、y和z,如果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方法和==操作符。当然,以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;
}
你看,在这里,String的equals并不是只是判断两个hashcode是否相等。而是在两个hasncode不等的情况下,去判断内容是否相等,如果相等,则返回true,不等,则返回false。而==操作符只判定hashcode是否相等,如果相等,则true,不等,则false。请看示例:
.
3.hashcode()
这个方法是返回制定对象的hashcode,在Object中默认以内存地址作为hashcode。那么我们在上面说,如果hashcode相等,但两对象未必相等,这是为什么呢?因为hashcode是根据hash算法进行生成的,当我们重写这个方法时,可能因为算法选取原因,返回的值有可能相同。比如我们本来打算返回的是变量i+1。但是最后不小心写成了1+1,这就一直返回这个常量2了。另外,另外,也正是因为这个原因,我们需要明确一点儿:hashcode不等同于内存地址。他们之间的关系就类比于形参和实参。内存地址是不会变的,但hashcode却是能够发生改变的。
关于这个知识点,需要长时间的使用才能得心应手,但是却是十分的让人头疼,有的人甚至不知道该怎么下手去接触它,但它又切切实实地提高开发的效率。所谓正则表达式,就是字符串格式匹配规则。我们可以用它来监测邮箱信息、身份证信息、手机号信息等等这些数据是否合法,一般用于数据监测。那么它是怎么匹配的呢?我们看看它的匹配定义
正则表达式 匹配的字符串
-------------- -------------------
k k
abc abc
[abc] a, b, c
[abc][123] a1,a2,a3,b1,b2,b3,c1,c2,c3
[a-z] a,z,k,r,h
[a-zA-Z_0-9] a,A,9,0,3,_
[^a-zA-Z] 8,1,0,&,$,*
\d 数字
\D 排除数字
\w 单词字符[a-zA-Z_0-9]
\W 排除单词字符
\s 空白字符
\S 排除空白字符
. 任意字符
[\u4e00-\u9fa5] 中文范围
[abc]? ? 0或1个a,b,,c
[abc]?[123] a1, b2, 3
[abc]* * 0到多个
[abc]+ + 1到多个
a,b,c,aabbccabcabcabcbcabcccbcacbca
[abc]{3} aaa,abc,bbb,abc,cba,bac,ccc
[abc]{3,5} abc,abca,abcab
[abc]{3,} abc,abca,abcabccbacbccacbcaaa
关于正则表达式,可以用在多种编程语言上,java中关于它的类有以下两个:
* Pattern 封装一个正则表达式
* Matcher 封装正则表达式和要匹配的字符串
创建对象的方式如下:
第一步: Pattern p = Pattern.compile(正则表达式);
第二步:Matcher m = p.matcher(要匹配的字符串);
关于这个知识点,一般不需要多掌握,但是也必须理解。我们或许不需要掌握匹配规则,但我们需要学会怎么去监测数据的合法性。以下是Matcher 类的几个方法:
【1】find() 向后查找下一段匹配的子串,返回字符串
【2】find(int from) 从 from 位置向后查找
【3】group() 取出刚刚找到的子串
【4】start() 取出刚刚找到的子串的起始位置
【5】end() 取出刚刚找到的子串的结束位置,end 位置不包含
当然,接下来是用于正则表达式的相关方法
【1】matches(正则表达式)
判断当前字符串,能否与正则表达式匹配
【2】replaceAll(正则表达式, 子串)
将找到的匹配子串,替换为新的子串
【3】split(正则表达式)
用匹配的子串,拆分字符串
其实本来介绍更多的java类库的,但发现当初剖析一个类,不如在特定的场景里面去进行剖析。所以,关于相关类库的介绍,只阐述这几个比较特别的类。有兴趣的同学可以去看一下java的官方文档,了解更多的知识点。这是一种进阶,但也是一种基础。我们下一章的内容是:java-深入-类集(核心知识点),敬请关注。如有疑问,请在下方指出,共同完善,谢谢。