前言
《Thinking in Java》做为Java最经典的学习书籍之一,不论是对于学习java的新手或是有一定经验的程序员来说都有不同的学习价值,在工作的这两年多当中由于种种杂事一直没时间拜读此书,近期决定坚持每天抽空细读一下,一方面巩固一下java基础,另一方面要找一下学习的状态,每天忙于项目不停赶进度写代码而忽略了学习也是不行的,所以感觉通过写blog来坚持读书学习也是很不错的,本系列blog参照的是《Java编程思想第4版》,第一章“对象导论”简要介绍了Java语言的一些重要特性和知识点,我们从第二章“一切都是对象”开始记录。
用引用操纵对象
如题,这一小节介绍了编程语言操纵内存中元素的方式,包括:
- 直接操纵——Java中的引用(reference)
- 间接操纵——C&C++中的指针(pointer)
和C以及C++相比,这些在Java中得到了简化,因为一切都被视为对象,而操纵对象的标识符也就是对象的一个引用(reference)了。例如:
String s;
这样就创建了一个String对象的引用——s,注意此时并没有对象被创建。作者建议更安全的做法是创建引用的同时初始化对象,例如:
String s = "asdf";
这里展示了一个Java语言的特性:字符串可以用带引号的文本初始化。
必须由你创建所有对象
这里提到了创建对象的通用方式——new关键字。new关键字的意思是“给我一个新对象”,上面的例子同样可以这样写:
String s = new String("asdf");
尽管这些显而易见,我们很清楚String类提供了这样的一个构造方法,但作者必然是按照循序渐进的思路去介绍知识点,所以跟上作者的思路来继续看下去。
存储到什么地方
这里介绍了部分硬件相关的知识点,即:对象在内存中是怎样放置和存储?下面提到了5个可以存储数据的地方:
- 寄存器。
在学习计算机基础时我们都知道寄存器位于CPU(*处理器)中,而且它是最快的存储区,但是寄存器的数量有限,它是根据需求自动分配,Java语言无法控制。 - 堆栈。
位于RAM(随机访问存储器),其速度仅次于寄存器,堆栈指针向下移动,则分配新内存;若向上移动,则释放那些内存。某些Java数据会存储于堆栈中,特别是对象的引用,但这里强调了Java对象并不会存储在这里。 - 堆。
也位于RAM区,是一种通用的内存池,用于存放所有Java对象。在堆中分配存储相较于堆栈更具灵活性,但代价是效率会低于堆栈。 - 常量存储。
常量值通常直接存放在程序代码内部,它们永远不会被改变。在嵌入式操作系统中,可以选择放在ROM(只读存储器)中。 - 非RAM存储。
分为流对象和持久化对象,即不受程序的任何控制。流对象就是将对象转换为字节流进行传输,而持久化对象也就是常用的将对象存储在数据库中了,例如在Java中常用的JDBC和Hibernate。
特例:基本类型
在Java中基本类型不用通过new关键字来创建(同C&C++一致),而是通过直接声明的方式去创建,例如:
char c = 'x';
这里的c是一个并非是引用的“自动变量”,这个变量直接存储“值”,并置于堆栈中,因此更加高效。简单总结一下:
- 创建基本类型无需new关键字,这一点和C以及C++一致。
- 基本类型创建后并无引用(reference),它应视为一个“自动”变量,并直接将值存储在堆栈中(普通对象均存储在堆中)。
Java要确定每种基本类型所占存储空间的大小(不像其它大多数语言随机器硬件架构变化而变化,这也是更具可移植性的原因之一)。
下面看一下Java中所有基本类型的大小范围:
基本类型 | 大小 | 最小值 | 最大值 | 包装器类型 |
---|---|---|---|---|
boolean | - | - | - | Boolean |
char | 16-bit | Unicode 0 | Unicode 2^16-1 | Character |
byte | 8 bits | -128 | +127 | Byte |
short | 16 bits | -2^15 | +2^15-1 | Short |
int | 32 bits | -2^31 | +2^31-1 | Integer |
long | 64 bits | -2^63 | +2^63-1 | Long |
float | 32 bits | IEEE754 | IEEE754 | Float |
double | 64 bits | IEEE754 | IEEE754 | Double |
void | - | - | - | Void |
在Java中所有数值类型都有正负号(而C语言中有无符号类型unsigned)。
另外,基本类型也提供了包装器对象(Wrapper),它和普通对象一样在堆中被创建,每一种包装器都对应一个基本类型。例如:
Character ch = new Character('x');
关于包装器在后面的章节再细说,但显而易见的一点是,和普通类型相比它的效率会低一些(存储堆栈和堆的区别),但同时这种代价会允许它做更多的事情。
Java中还提供了两个用于高精度计算的类:BigInteger 和 BigDecimal:
- BigInteger支持任意精度的整数运算。
- BigDecimal支持任意浮点数运算(例如货币运算)。
Java中的数组
这里仅仅强调了Java数组的安全性(和C&C++做对比),即:Java确保数组会被初始化,而且不能在它的范围之外被访问。基本类型的数组初始化由编译器来保证(将数组所占的内存全部置零),对象数组则是相当于创建了一个引用数组,且每个引用都会初始化为null,若试图使用null引用则会报运行时异常,因此常见的数组错误在Java中可以避免。
永远不要销毁对象
作用域
大多数过程性语言都有作用域概念(scope),在Java和C&C++中,作用域均是由花括号的位置决定的,例如:
{
int x = 12;
// Only x available
{
int q = 96;
// Both x & q available
}
// Only x available
// q is "out of scope"
}
这里还提到了一点和C&C++的区别,在C&C++里将一个较大作用域的变量“隐藏”起来的做法在Java中是不允许的,例如:
{
int x = 12;
{
int x = 96; // Duplicate local variable x
}
}
对象的作用域
Java对象不具备和基本类型一样的生命周期,当用new创建一个Java对象时,它可以存活于作用域之外,简言之,就是说一旦创建了一个对象,尽管对象的引用在作用域终点会消失,但该引用所指的对象依然会占据内存空间。为了避免创建的N多个对象占满内存,Java提供了垃圾回收器,它用来监视用new创建的所有对象,并辨别不会再被引用的对象并释放其空间(消除了C++中相关的类似问题)。
创建新的数据类型:类
大多数面向对象的程序设计语言习惯用关键字“class”来表示“我准备告诉你一种新类型的对象看起来像什么样子”,Java也不例外,用class关键字来声明一个新的类,例如:
class ATypeName{
// Class body goes here
}
同样也可以用new关键字来创建一个这种新类型的对象:
ATypeName a = new ATypeName();
字段和方法
一旦在Java中定义了一个类,就可以在类中设置两种类型的元素:
- 字段(数据成员)
- 方法(成员函数)
字段可以是任何类型,每个对象都有用来存储其字段的空间,普通字段不能在对象间共享。例如:
class DataOnly{
int i;
double d;
boolean b;
}
可以给对象的字段赋值,具体实现为:在对象引用的名称之后紧接着一个句点,然后再接着是对象内部的成员名称objectReference.member
,例如:
DataOnly data = new DataOnly();
data.i = 47;
data.d = 1.1;
data.b = false;
此时的DataOnly类除了保存数据外再也没有别的用处,因为它没有任何成员方法(关于成员方法后面再提)。
若类的某个成员是基本数据类型,即使没有初始化,Java也会确保它获得一个默认值,所有基本类型的默认值如下所示:
基本类型 | 默认值 |
---|---|
boolean | false |
char | ‘\u0000’(null) |
byte | (byte)0 |
short | (short)0 |
int | 0 |
float | 0.0f |
double | 0.0d |
注意,当变量作为类的成员(全局变量)时,Java才确保给定其默认值进行初始化,并不适用于“局部变量”(方法中的变量),所以若是局部变量没有初始化就使用的话,那么Java会在编译时返回一个错误,这一点也是Java优于C++的地方。
方法、参数和返回值
C和C++中都用函数(function)这个词来描述命名子程序,在Java中用方法(method)这个词来表示“做某些事情的方式”。Java的方法决定了一个对象能够接收什么样的消息。方法的基本组成部分包括:方法名、参数、返回值和方法体,基本形式如下:
ReturnType methodName( /* Argument list */ ) {
/* Method body */
}
方法名和参数列表(合起来被称为“方法标签”)唯一的标识出某个方法。
Java中的方法只能作为类的一部分来创建,方法通过对象来调用(静态方法除外),调用方法时,需要先列出对象名,紧接着是句点,然后是方法名和参数列表,如下所示:
objectName.methodName(arg1, arg2, arg3);
再举一个简单的例子:
int x = a.f();
上述方法f没有参数,返回值为int类型,是对象a的一个成员方法,这种方法调用的行为通常被称为发送消息给对象,这里消息是f(),对象是a,面向对象的程序设计通常简单的归纳为“向对象发送消息”。
参数列表
方法的参数列表指定要传递给方法什么样的信息,在参数列表中必须指定每个所传递对象的类型和名字。例如:
int storage(String s){
return s.length()*2;
}
此方法告诉你,需要多少个字节才能容纳一个特定的String对象中的信息。
若某个方法的返回类型是void,那么这个方法中的return关键字的作用只是用来退出方法,例如:
void nothing(){
return;
}
因此,没有必要到方法结束时才离开,可以在任何地方返回。但若返回类型不是void,那么无论在何处返回,编译器都会强制返回一个正确类型的返回值。
构建一个Java程序
在构建第一个Java程序之前需要了解一些问题。
名字可见性
和C以及C++相比,Java通过包(package)来解决命名冲突的问题,即每个类都应放在一个包中,而包的格式应当是自己的Internet域名的反写,例如:net.mindview.utility.foibles,而句点用来代表子目录的划分。在Java 1.0和Java 1.1中,扩展名com、org、edu、net等约定为大写,而在Java 2开发到一半时,设计者又发现会引起一些问题于是又都换回小写。
运用其它构件
如果想在程序中使用预先定义好的类,那么编译器就必须知道怎么定位它,在Java中可以使用import关键字来准确的告诉编译器你想要的是什么类。import指示编译器导入一个包,也就是一个类库(在其它语言中,一个库不仅包含类,还可能包括方法和数据,但Java中所有的代码都必须写在类里)。例如:
import java.util.ArrayList;
也可以使用通配符来一次导入一群类:
import java.util.*;
static关键字
考虑以下两种情形:
- 只想为某特定域分配单一存储空间,而不去考虑究竟要创建多少个对象,甚至根本不创建任何对象。
- 希望某个方法不与包含它的类的任何对象相关联,也就是说即使没有创建对象也能调用这个方法。
通过使用static关键字可以满足上面两点。当声明一个域或方法为static时,就表示这个域或方法不会与它所属的类的任何对象相关联。所以即使某个类没有创建任何对象,也可以访问它的static域或调用它的static方法。只须将static关键字放在定义之前,就可以将字段或方法设定为static,例如:
class StaticTest{
static int i = 47;
}
现在,即使创建了2个StaticTest对象,StaticTest.i也只有一份存储空间,这两个对象会共享同一个i。
引用static变量有两种方式,一种是通过对象去定位,另一种是通过类名直接引用。例如:
StaticTest st = new StaticTest();
int i1 = st.i; // 通过对象定位
int i2 = StaticTest.i; // 通过类名直接引用(推荐)
System.out.println(i1==i2); //true
使用类名直接引用是引用static变量的首选方式,这不仅是因为它强调了变量的static结构,而且在某些情况下它还为编译器进行优化提供了更好的机会。
类似的逻辑也应用于静态方法,例如:
class Incrementable{
static void increment(){
StaticTest.i++;
}
}
System.out.println(StaticTest.i); // 47
Incrementable.increment();
System.out.println(StaticTest.i); // 48
如上所示,通过调用Incrementable的静态方法increment()使得StaticTest的静态变量i加了1。
static方法的一个重要用法就是在不创建任何对象的前提下就可以调用它,这一点对定义main()方法很重要,main()方法是运行一个应用时的入口点。
你的第一个Java程序
从简单的打印日期和字符串开始:
import java.util.*;
public class HelloDate(){
public static void main(String[] args){
System.out.println("Hello,it's: ");
System.out.println(new Date());
}
}
首先,import语句用于引入“额外”的类,那么什么是额外的类,其实就是除了java.lang包外的所有类了,用于lang包下的类比较常用所以Sun已经为我们自动导入到了每一个Java文件中了。下面再看第5行,System类有许多属性,这里看到的out是一个静态的PrintStream对象,由于是静态的所以它可以直接被类引用,println是PrintStream对象的方法,它的作用是“将我给你的数据打印到控制台,完成后换行”。
在Java中类的名字必须和文件名相同,若是想创建一个如上一样可以独立运行的程序,那么还必须包含一个main()方法,形式如下:
public static void main(String[] args){
}
public关键字指的是一个可由外部调用的方法,main()方法的参数是一个String数组,Java编译器要求必须有这个参数,因为它要用来存储命令行参数。
第6行中传递的参数是一个Date对象,一旦创建它之后,就可以直接将它的值(它被自动转换为String类型)发送给println()。当这条语句执行完毕后,Date对象就不再被使用,而垃圾回收器会发现这一情况,并在任意时候将其回收。
再看一下System类的一些其它方法:
System.getProperties().list(System.out);
System.out.println(System.getProperty("user.name"));
System.out.println(System.getProperty("java.library.path"));
第一行中System类的静态方法getProperties()可以获取当前运行程序的操作系统的所有“属性”,主要是环境信息,例如:系统用户名、jdk版本等等,它返回的是一个Properties对象,而Properties对象的list方法则是结果发送给它的参数:这里我们传的是Sytem.out(标准输出流),所以可以直接在控制台显示。而第二行和第三行中System类的静态方法getProperty()则是根据指定的key去查询对应的属性值。
编译和运行
要编译和运行首先必须要有一个Java开发环境,比如Sun提供的免费的JDK(Java Developer’s Kit),或者由IBM提供的jikes编译器(速度快于javac)。下面是使用javac编译.java文件的例子(注意执行命令前需要切换到java文件所在的目录,同时要确保计算机能找到javac和java这两个文件):
javac HelloDate.java
编译成功后会在当前目录生成.class的字节码文件,然后再通过java运行即可:
java HelloDate
现在就可以看到控制台的输出信息了。
注释和嵌入式文档
在Java中有两种注释风格,它们都源于C和C++,分别是:
- 以/*开始并以*/结束,可用于多行注释。
- 以一个//开头直到句末,仅用于单行注释。
在多行注释中/*和*/之间的所有东西都会被忽略(编译时),所以一下两种写法没有区别:
/* This is a comment
* that continues
* across lines
* /
/* This is a comment
that continues across lines */
注释文档
关于程序文档的生成,Java提供了javadoc这个工具,它是JDK安装的一部分。javadoc输出的是一个HTML文件,该工具使得我们只需创建和维护单一的源文件,就能自动生成有用的文档了。
语法
所有的javadoc命令都只能在/**注释中出现,和通常一样,注释结束于*/。使用javadoc的方式主要有两种,分别是:
- 嵌入HTML
- 文档标签
独立文档标签是一些以@字符开头的命令,且要置于注释行的最前面。而行内文档标签则可以出现在javadoc注释中的任何地方,它们也是以@字符开头,但要括在花括号内。共有三种类型的注释文档,分别作用于类、域和方法,例如:
/** A Class comment */
public class Documentation1 {
/** A field comment */
public int i;
/** A method comment */
public void f() {
}
} // /":~
javadoc只能为public和protected成员进行文档注释,这样做是因为只有这两种成员才能在文件之外被使用,这也是客户端程序员所期望的。上述代码的输出结果是一个HTML文件,它与其它Java文档具有相同的标准格式。首先将这个Java类拷贝到一个新创建的目录中,然后切换到当前目录执行javadoc命令即可:
如上图所示,这样就已经成功创建了文档,再看一下刚才新创建的目录下的文件列表:
打开index.html即可看见我们熟悉的格式的文档了:
嵌入式HTML
javadoc通过生成的HTML文档传送HTML命令,这使你能够充分利用HTML。当然,其主要目的还是为了对代码进行格式化,例如在上面的例子中的f()方法上加入如下注释:
/** A method comment
* You can <em>even</em> insert a list:
* <ol>
* <li> Item one
* <li> Item two
* <li> Item three
* </ol>
* */
public void f() {
}
再次运行javadoc重新生成注释文档,那么可以看到这个方法的注释已经包含了一定的格式:
注意,不要在嵌入式HTML中使用标题标签,例如:<h1>
或<hr>
,因为javadoc会插入自己的标题,而你的标题可能同它们发生冲突。所有类型的注释文档都支持嵌入式HTML(类、域和方法)。
一些标签示例
这里将介绍一些可用于代码文档的javadoc标签。
1.@see: 引用其它类
@see标签允许用户引用其它类的文档。javadoc会在其生成的HTML文件中,通过@see标签链接到其它文档,格式如下:
@see classname
@see fully-qualified-classname
@see fully-qualified-classname#methodname
上述每种格式都会在生成的文档中加入一个具有超链接的“See Also”(参见)条目,但是javadoc**不会检查**你所提供的超链接是否有效。在java源码中可以容易的找到这种标签:
* @see java.lang.Object#toString()
* @see java.lang.StringBuffer
* @see #String(byte[], int)
2.{@link package.class#member label}
该标签与@see极其相似,只是它作用于行内,并且是用“lable”作为超链接文本而不用“See Also”。
3.{@docRoot}
该标签产生到文档根目录的相对路径,用于文档树页面的显示超链接。
4.{@inheritDoc}
该标签从当前这个类的最直接的基类中继承相关文档到当前的文档注释中。
5.@version
用于描述版本,格式如下:
@version version-infomation
如果javadoc命令行使用了“-version”标记,那么就从生成的HTML文档中特别提取出版本信息。
6.@author
用于描述作者信息,格式如下:
@author author-infomation
如果javadoc命令行使用了-author标记,那么就从生成的HTML文档中特别提取作者信息。可以使用多个标签,以便列出所有作者,但是它们必须连续放置。全部作者信息会合并到同一段落,置于生成的HTML中。例如:
/**
*
* @see java.lang.StringBuffer
*
* @author Lee Boynton
* @author Arthur van Hoff
* @author Martin Buchholz
* @author Ulf Zibis
*
* */
public class Documentation1 {}
然后运行javadoc命令行并带上-version标记,即可看到生成的HTML中的作者信息:
7.@since
该标签允许你指定程序代码最早使用的版本,可以在HTML Java文档中看到它被用来指定所用的JDK版本的情况。例如:
@since JDK1.0
这就表示此类是从JDK1.0版本开始使用的。
8.@param
用于描述方法的参数,该标签用于方法文档中,形式如下:
@param parameter-name description
parameter-name是方法的参数列表中的标识符,description是可延续数行的文本,终止于新的文档标签出现之前。
9.@return
用于描述方法返回值,该标签用于方法文档,格式如下:
@return description
10.@throws
用于描述某个方法可能会产生的异常以及异常说明,格式如下:
@throws fully-qualified-class-name description
fully-qualified-class-name给出一个异常类的无歧义的名字,而该异常类在别处定义。description告诉你为什么此特殊类型的异常会在方法调用中出现。例如:
* @throws PatternSyntaxException
* if the regular expression's syntax is invalid
11.@deprecated
用于描述过期(已由改进的新特性取代,不建议再使用)的方法,如果使用一个标记为@deprecated的方法,则会引起编译器发布警告。在Java SE5中,javadoc标签@deprecated已经被@Deprecated注解所替代。
文档示例
下面看一段加了完整文档注释的Java程序:
import java.util.*;
/**
* The first java program. Display a string and today's date.
*
* @author Wang Liang
* @version 1.0.0
*/
public class HelloDate {
/**
* Entry point to class & application.
*
* @param args
* array of string arguments
* @throws exceptions
* No exceptions thrown
*/
public static void main(String[] args) {
System.out.println("Hello,it's: ");
System.out.println(new Date());
}
} /* Output: (55% match)
Hello,it's:
Thu Mar 24 11:29:36 CST 2016
*/// :~
编码风格
在“Java编程语言编码约定”中,代码风格是这样规定的:类名的首字母要大写,如果类名由几个单词构成,那么把它们并在一起,其中每个内部单词的首字母都要采用大写形式,即“驼峰风格”,例如:
class AllTheColorsOfTheRainbow{ // ...
几乎其它所有内容——方法、字段(成员变量)以及对象引用名称等,公认的风格与类一致,只是标识符的第一个字母采用小写,例如:
class AllTheColorsOfTheRainbow{
int anIntegerRepresentingColors;
void changeTheHueOfTheColor(int newHue){
// ...
}
// ...
}
总结
第一章的内容到此就结束了,整体非常简单,重点是文档注释方面的内容,还有一些计算机基础方面和部分历史方面的知识仅作了解,本系列blog是我利用闲暇时间记录的读书笔记,关于书中没有讲到的例子我这里也都给出了实例并进行了细化,旨在复习一遍Java基础,希望对有同样想法的朋友有所帮助,The End。