软件构造心得1
在我写实验的时候,常常被字符串为空和字符串相等的判断给坑到,而在进行调试时,还遇上了一些难以理解的现象,于是我大致整理了下遇到的问题,写了这篇博客。
一、字符串为空
常见的判断字符串为空,主要有以下三种
String string;
string == null //1
string == "" //2
string.isEmpty(); //3
1、string == null
这条语句的意思是string
指的对象为空,也就还没分配内存。
String string = null;
System.out.println(string == null);
string = "";
System.out.println(string == null);
得到的结果
true
false
可见一旦分配了对象,哪怕是空的字符串""
,string == null
也会判断为false
。
值得注意的是,由于string还未分配内存,因此不能直接调用string.equals() string.isEmpty()
等方法,否则会报空指针错误
2、string.equals("")
或string == ""
这条语句的意思是字符串的值为空,也就是分配了对象,但分配的对象是一个空的字符串""
。
也就是
String string = "";
System.out.println(string == "");
打印结果是
true
其实对于字符串内容的比较,java中更推荐使用string.equals("")
来进行比较,尽管对于我们直接写的""
字符串内容,java虚拟机是将其存在常量缓冲池中,不需要分配地址,所以使用==
的实际效果是和equals()
方法一样的,但是如果我们进行比较的内容是另一个字符串对象,那么==
将直接比较两个对象的地址,这样即使两个内容相同的字符串,比较的结果也是不同的。
直接说可能有些抽象,下面通过一个例子展示。
String str1 = new String();
str1 = "hello";
String str2 = new String("hello");
System.out.println(str1 == "hello");
System.out.println(str1 == str2);
System.out.println(str2 == "hello");
结果是
true
false
false
对于str1
,直接指向了缓冲池的"hello"
字符串,而str2
则被分配了新的对象,没有指向缓冲池,因此使用==
进行比较会出错。
而使用equals
进行比较,则是直接比较内容,就没有这方面的问题。
String str1 = new String();
str1 = "hello";
String str2 = new String("hello");
System.out.println(str1.equals("hello"));
System.out.println(str1.equals(str2));
System.out.println(str2.equals("hello"));
结果
true
true
true
对于这块想要更深入了解可以看对象等价性和引用等价性的区别。
3、string.isEmpty()
其实string.isEmpty()
和string.equals("")
是等价的。
isEmpty
的代码内部实现如下
/**
* Returns {@code true} if, and only if, {@link #length()} is {@code 0}.
*
* @return {@code true} if {@link #length()} is {@code 0}, otherwise
* {@code false}
*
* @since 1.6
*/
public boolean isEmpty() {
return value.length == 0;
}
只有当字符串为空串时,也就是""
,字符串的长度为0。
二、空字符串的打印
对于下面的代码
String str = null;
System.out.println(str);
我本以为静态检查会报错,一个空的对象是没法打印的。
但实际上打印的结果是
null
我们先看println
的源码
/**
* Prints a String and then terminate the line. This method behaves as
* though it invokes {@link #print(String)} and then
* {@link #println()}.
*
* @param x The {@code String} to be printed.
*/
public void println(String x) {
if (getClass() == PrintStream.class) {
writeln(String.valueOf(x));
} else {
synchronized (this) {
print(x);
newLine();
}
}
}
接着看printstream
的内部实现
/**
* The following two methods exist because in, out, and err must be
* initialized to null. The compiler, however, cannot be permitted to
* inline access to them, since they are later set to more sensible values
* by initializeSystemClass().
*/
private static InputStream nullInputStream() throws NullPointerException {
if (currentTimeMillis() > 0) {
return null;
}
throw new NullPointerException();
}
private static PrintStream nullPrintStream() throws NullPointerException {
if (currentTimeMillis() > 0) {
return null;
}
throw new NullPointerException();
}
阅读spec可以大致知道, in、out和err初始都是null的,它们稍后会被initializeSystemClass()
设置为更合理的值。
而这个initializeSystemClass()
的spec是这样的
/**
* Initialize the system class. Called after thread initialization.
*/
initializeSystemClass()
这个方法的具体细节不再赘述了,有兴趣可以去看system类的源码。
总而言之,println相当于实现了以下内容
public void println(String str) {
if (str == null) {
str = "null";
}
write(s);
}
事实上,对于Object o = null;
打印结果也仍然是null
而如果是运行下面内容
char[] ch = null;
System.out.println(ch);
则会抛出异常
Exception in thread "main" java.lang.NullPointerException
at java.base/java.io.Writer.write(Writer.java:213)
at java.base/java.io.PrintStream.writeln(PrintStream.java:608)
at java.base/java.io.PrintStream.println(PrintStream.java:940)
如果是直接写System.out.println(null);
,则在静态检查阶段就会报错。
三、空字符串的连接
在写实验3时,我曾利用string == null
来判断计划项是否被分配资源,若分配了便采用字符串连接,试图将资源赋值给string
(像高铁管理这种资源由多节车厢组成的,采用字符串连接可能会方便点),但却没达到我想要的结果。
实验的代码相当于以下代码
String str = null;
str = str + "hello";
System.out.println(str);
我本以为会打印
hello
但实际打印的结果却是
nullhello
这实际上是字符串连接导致的问题。
通过反编译Java的代码,+
的实现实际上是通过StringBuilder
的append
方法来进行字符串连接,而append
的实现大致如下
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
可见对于空对象str,它进行了特判,转为"null"
,然后进行连接。
四、窗体中的JTextField
在实验3中用户的所有操作都通过GUI实现,因此绕不开窗体,由于我想加入对于用户输入的判断,如果用户没有对窗体的每个文本框都输入内容,需要提示用户重新输入。因此我想知道:
对于窗体的JTextField
,如果我们不输入内容,调用下面的代码
private JTextField textField = new JTextField();
String str = textField.getText();
str
存的是空指针null
还是空字符串""
呢?
我们查看getText()
的源码
/**
* Returns the text contained in this <code>TextComponent</code>.
* If the underlying document is <code>null</code>,
* will give a <code>NullPointerException</code>.
*
* Note that text is not a bound property, so no <code>PropertyChangeEvent
* </code> is fired when it changes. To listen for changes to the text,
* use <code>DocumentListener</code>.
*
* @return the text
* @exception NullPointerException if the document is <code>null</code>
* @see #setText
*/
public String getText() {
Document doc = getDocument();
String txt;
try {
txt = doc.getText(0, doc.getLength());
} catch (BadLocationException e) {
txt = null;
}
return txt;
}
可见字符串只有遇到BadLocationException
异常才会返回为null,而看BadLocationException
的spec
/**
* This exception is to report bad locations within a document model
* (that is, attempts to reference a location that doesn't exist).
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans™
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*
* @author Timothy Prinzing
*/
只有尝试引用不存在的位置才会抛出此异常,我个人觉得这个引用不存在的位置语义不是很明确,因此继续看Document
的getText
方法何时抛出BadLocationException
/**
* Gets a sequence of text from the document.
*
* @param offset the starting offset >= 0
* @param length the number of characters to retrieve >= 0
* @return the text
* @exception BadLocationException the range given includes a position
* that is not a valid position within the document
* @see Document#getText
*/
public String getText(int offset, int length) throws BadLocationException {
if (length < 0) {
throw new BadLocationException("Length must be positive", length);
}
String str = data.getString(offset, length);
return str;
}
可以看到是length < 0
才抛出异常,而这个length
是JTextField
类的getText()
的doc.getLength()
传进来的,所以我们再看Document
的getLength
方法
/**
* Returns the length of the data. This is the number of
* characters of content that represents the users data.
*
* @return the length >= 0
* @see Document#getLength
*/
public int getLength() {
return data.length() - 1;
}
接着看这个data.length()
的源代码
/**
* Returns the length of the content.
*
* @return the length >= 1
* @see AbstractDocument.Content#length
*/
public int length() {
return count;
}
可以看到返回的是count
问题就是count
初始的值为什么?
向上翻到了如下的初始化代码
/**
* Creates a new StringContent object, with the initial
* size specified. If the length is < 1, a size of 1 is used.
*
* @param initialLength the initial size
*/
public StringContent(int initialLength) {
if (initialLength < 1) {
initialLength = 1;
}
data = new char[initialLength];
data[0] = '\n';
count = 1;
}
可见count
初始化为1
因此我们不输入数据,data.length() - 1 = 0
,不会抛出BadLocationException
,故得到的字符串str
就不会是空指针null
,那么str
很有可能是空字符串""
了
下面接着验证,通过上面Document
的getText
方法,我们知道返回的字符串的值是通过data.getString(offset, length);
得到的,于是查看data.getString
源代码
/**
* Retrieves a portion of the content. where + len must be <= length().
*
* @param where the starting position >= 0
* @param len the length to retrieve >= 0
* @return a string representing the content; may be empty
* @exception BadLocationException if the specified position is invalid
* @see AbstractDocument.Content#getString
*/
public String getString(int where, int len) throws BadLocationException {
if (where + len > count) {
throw new BadLocationException("Invalid range", count);
}
return new String(data, where, len);
}
到这边就一清二楚了,由于我们不输入数据,所以传入的参数len
是0
,于是返回的新String
对象创建的长度是0
,所以就是空字符串""
。