0%

The Basic Concepts of Java

Java is a good Coffee for Programmer
Resolve Problems from the views of Java designer , Compiler, CPU, Memery…

见过最多的,也最可悲的是—— 永远不相信自己所有的问题就出在自己从来都不肯耐心把最基础的东西弄清楚弄明白。[1]

On job Training[2]

启发编程思想,培养编程感觉

用程序来解决生活中的实际问题

How –> Why

Java virtual machine

可运行Java字节码的假想计算机

javac:编译器,在javac命令中,可以使用通配符来指定一次编译多个源文件
java: 解析器,启动一个Jvm

javac 将java源程序翻译为jvm可执行代码——java字节码,这一编译过程同c/c++有些不同,当C编译器生成一个对象的代码时,该代码是为在某一特定硬件平台运行而产生的。因此,在编译过程中,编译程序通过查表将所有对符号的引用转换为特定的内存偏移量,以保证程序的运行。

java编译器却不将对变量和方法的引用编译为数值引用,也不确定程序执行过程中的内存布局,而是将这些符号引用信息保留在字节码中,由解析器在运行过程中创立内存布局,然后再通过查表来确定一个方法所在的地址,这样就有效保证了java的可移植性和安全性。

运行jvm字节码的工作是由解析器来完成

  1. 代码的装入

由class loader完成,其负责装入运行一个程序需要的所有代码,这也包括程序代码中的类所继承的类和被其调用的类。当类装载器装入一个类时,该类被放在自己的名字空间中。除了通过符号引用自己名字空间以外的类,类之间没有其他办法可以影响其他类。在本台计算机上的所有类都在同一地址空间内,而所有从外部引进的类,都有一个自己独立的名字空间。这使得本地类通过共享相同的名字空间获得较高的运行效率,同时又保证它们与从外部引进的类不会相互影响。

当装入了运行程序需要的所有类后,解析器便可确定整个可执行程序的内存布局。解析器为符号引用于特定的地址空间建立对应关系及查询表。通过在这一阶段确定代码的内存布局,java很好地解决了由超类改变而使子类崩溃的问题,同时也防止了代码对地址的非法访问。

  1. 代码的校验
    被装入的代码由字节码校验器进行检查,校验器可发现操作数栈溢出、非法数据类型转化等多种错误。通过校验后,代码便开始执行。

  2. 代码的执行
    Java字节码的执行有两种方式:
    A、即时编译方式
    解析器先将字节码编译成机器码,然后再执行该机器码。
    B、解析执行方式
    解析器通过每次解析并执行一小段代码来完成java字节码程序的所有操作。
    通常采用的是第二种方法。由于jvm规格描述具有足够的灵活性,这使得将字节码翻译为机器代码的工作具有较高的效率,对于那些对运行速度要求较高的应用程序,解析器可将java字节码即时编译为机器码,从而很好地保证了java代码的可移植性和高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Usage: java [-options] class [args...]
(to execute a class)
or java [-options] -jar jarfile [args...]
(to execute a jar file)
where options include:
-d32 use a 32-bit data model if available
-d64 use a 64-bit data model if available
-server to select the "server" VM
The default VM is server.

-cp <class search path of directories and zip/jar files>
-classpath <class search path of directories and zip/jar files>
A ; separated list of directories, JAR archives,
and ZIP archives to search for class files.
-D<name>=<value>
set a system property
-verbose:[class|gc|jni]
enable verbose output
-version print product version and exit
-version:<value>
Warning: this feature is deprecated and will be removed
in a future release.
require the specified version to run
-showversion print product version and continue
-jre-restrict-search | -no-jre-restrict-search
Warning: this feature is deprecated and will be removed
in a future release.
include/exclude user private JREs in the version search
-? -help print this help message
-X print help on non-standard options
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
enable assertions with specified granularity
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
disable assertions with specified granularity
-esa | -enablesystemassertions
enable system assertions
-dsa | -disablesystemassertions
disable system assertions
-agentlib:<libname>[=<options>]
load native agent library <libname>, e.g. -agentlib:hprof
see also, -agentlib:jdwp=help and -agentlib:hprof=help
-agentpath:<pathname>[=<options>]
load native agent library by full pathname
-javaagent:<jarpath>[=<options>]
load Java programming language agent, see java.lang.instrument
-splash:<imagepath>
show splash screen with specified image
See http://www.oracle.com/technetwork/java/javase/documentation/index.html for more details.

Java Garbage Collector

Java类的实例对象和数组所需的存储空间是在堆上分配的,解析器具体承担为类实例分配空间的工作。解析器在为一个实例对象分配完存储空间后,便开始记录对该实例对象所占用的内存区域的使用。一旦对象使用完毕,便将其回收到垃圾箱中。

在java语言中,除了new语句外没有其他方法为一个对象申请和释放内存。对内存进行释放和回收的工作是由java运行体统承担的,这允许java运行系统的设计者自己决定碎片回收的方法。

在java程序运行过程中,一个垃圾回收器会不定时地被唤起检查是否有不再被使用的对象,并释放它们占用的内存空间,垃圾回收器不由程序员控制,也无规律可循,并不会一产生垃圾,它就被唤起,甚至有可能到程序终止,它都没有启动的机会。

不同的jvm采用不同的回收策略,一般有两种比较常用:

  1. 复制式回收策略
    先将正在运行中的程序暂停,然后把正在被使用的所有对象从他们所在的堆内存里复制到另一块堆内存,那些不再被使用的对象所占据的内存空间就被释放掉。

  2. 自省式回收策略
    检测所有正在使用的对象,并为它们标注,完成这项工作后再将所有不再被使用的对象所占据的内存空间一次释放。

Java把内存划分成两种,一种是栈内存,另一种是堆内存。

在方法中定义的一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配,当在一段代码块(也就是一对花括号{}之间)定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

堆内存用来存放由new创建的对象和数组,在堆中分配的内存,由java虚拟机的gc来管理,在堆中产生一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象,引用变量就相当于是为数组或对象起的一个名称(叫代号也行)。引用变量时普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放,而数组和对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它时,才会变为垃圾,不再被使用,但仍然占据内存空间不放,在随后一个不确定的时间呗GC释放,这也是java比较占内存的原因。

Java内部还是有指针的,只是把指针的概念对用户隐藏起来了。

Pitfall

Path

与java有关的环境变量对空格和中文是非常敏感的
java -verbose,显示JVM详细的加载过程

环境变量:OS中定义的变量,可供操作系统上所有应用程序使用
Windows:在cmd中,用set命令查看,是用户和系统环境变量的总和
path:设置供OS去寻找和执行应用程序的路径,当前目录–>Path设置的目录,以最先找到的为准
Windows,用;分隔,Linux用:分隔
Windows,用%variable%取值,Linux用$variable取值

classpath:jvm按照classpath环境变量指定的目录顺序去查找这个类,以最先找到的为准,注意:与linux类似,jvm只会到classpath指定的目录中去寻找类,而不会自动在当前目录中去寻找

.代表java虚拟机运行时的当前工作目录

系统Windows,用;分隔,Linux用:分隔

Java Class

Java中的程序必须以类的形式存在,一个类要被解析器直接启动运行,必须要有main方法,jvm运行时,首先会调用main方法,main方法的写法是固定的,public static void main(String[] args)

如果在class前没有public修饰符,命名可以是一切合法的名称,而带有public,必须与源文件同名

xxx.java中可以定义多个class,javac之后,产生多个class文件,能直接用java启动的只有那个含有main方法的类

Java Grammar

java是严格区分大小写的
java所有的程序代码分为结构定义和功能执行语句,一条语句可以写在若干行上,功能执行语句的最后必须用;结束。

java程序中一句连续的字符串不能分开在两行中写,如果太长,可以分成两个字符串,再用+号连起来,然后在+处断行。

1
2
3
单行注释//
多行注释/* */
文档注释/** */

多行注释可以嵌套单行注释,但是不能嵌套多行注释

标识符,不能以数字开头,不能是java中的保留关键字

正确的路有一条,错误的路千万条,何苦要去记住哪些错误的路呢?永远用字幕开头,尽量不要包含其他的符号。

注意:Java中没有sizeof、goto、const这些关键字,但不能用goto、const作为变量名。

常量

  1. 整型常量
    十六进制0x开头
    八进制0开头
    长整型l结尾

  2. 浮点数常量
    单精度f/F结尾
    双精度d/D结尾
    小数常量的默认类型为double
    float类型后面一定要加f/F

  3. 布尔常量
    true和false

  4. 字符常量
    java中的字符占用两个字节,是用unicode码表示的。
    单引号
    还可以用unicode码值来表示对应的字符

  5. 字符串常量

    1
    2
    3
    4
    5
    6
    7
    ""
    \反斜杠转义字符
    \\
    \r回车
    \n换行
    \t制表符
    \b退格键
  6. null常量
    表示引用的对象为空

变量

用一个变量定义一块内存以后,程序就可以用变量名代表这块内存中的数据。

1
2
int x=0,y;
y=x+3;

先取出x代表的那块内存单元的数,加上3,然后把结果放到y所在的那块内存单元。

Java基本变量类型,都是小写:
整数:byte,,short,int,long
浮点:float,double
字符:char (与c语言不同,java的字符占两个字节,是unicode编码的)
布尔:boolean

注意:java语言中没有无符号的数据类型

引用类型:类、接口、数组

计算机里只有数值,当你在内存中看到一个数值时,这个数值可能代表各种意义,比如你看到的文字、图像和听到的声音等都是使用数字形式来表示的,生活中的数值也可以代表其他意义,如1234可以代表密码、存款额、电报信息等,根据上下线索,我们能够知道这些数值代表的意义。其实字符也是一个数字,当要给一个字符变量赋值时,就可以直接用整数,如97对应字符’a’,使用char ch=97 将字符a赋值给变量ch,如果我们要将字符x赋值给一个char变量,该填一个怎样的整数呢?显然,不太容易记住每个字符所对应的数字,所以,我们就用单引号加上这个字符本身来表示那个字符对应的数字,如char ch=’x’。

变量的作用域(scope)
在c/c++/java里,一对花括号中间的部分就是一个代码块,代码块决定其中定义的变量的作用域。

局部变量在进行取值操作前必须被初始化或进行过赋值操作,否则会出现编译错误

基本数据类型之间的转换

  1. 自动(隐式)
    同时满足两个条件:
    A、两种类型彼此兼容;
    B、目标类型的取值范围要大于源类型。

  2. 强制(显式)
    类型不兼容或者取值范围要小于源类型

字符串可以使用+同其他的数据类型相连形成一个新的字符串(将其他数据类型值默认转化为十进制)

源和目标分别是两个大小不同的内存块(由变量及数据的类型来决定),将源数据赋值给目标内存的过程,就是用目标内存块去套取源内存中的数据,能套多少算多少。

  1. 表达式的数据类型自动提升
1
2
3
byte b = 5;
b = b-2;
System.out.println(b);

在表达式取值时,变量值被自动提升为int类型

b = (byte)(b-2)

java定义了若干适用于表达式的类型提升规则
A、所有的byte类型、short类型和char类型的值将被提升到int类型;
B、如果一个操作数是long类型,计算结果就是long类型
C、如果一个操作数是float类型,计算结果就是float类型
D、如果一个操作数是double类型,计算结果就是double类型

方法

方法要接收调用程序传递进来的参数,必须为每个传递进来的参数定义一个变量。如果方法没有return语句,编译时,系统会自动在方法的最后添加一个return。

void:不知道是什么类型,可定义方法时又非要填写一个返回值类型,就用它充数吧。

方法的重载(overload)

java的编译器很聪明,能够根据调用方法时所传递的参数的个数和类型选择相应的方法,重载方法的参数列表必须不同,要么是参数的个数不同,要么是参数的类型不同。重载方法的返回值类型可以相同,也可以不同。

如果两个方法的参数类型和个数完全一样,返回值类型不同,行不行?如果你是java的设计者,而且你的用户在程序里编写了这样的两个方法,在调用时,你能根据他所传递的参数来为他选择到底该用哪个吗?没有办法吧!那就是不能这样做。学编程不需要死记硬背,靠的是动脑筋来思考,这样的学习才能做到举一反三、触类旁通。

Java中的运算符

1
2
3
“+”除字符串相加功能外,还能降字符串与其他的数据类型相连成一个新的字符串,条件是表达式中至少有一个字符串,如"x"+123的结果是"x123"
如果对负数取模,可以把模数符号忽略不计,如5%(-2)=1,但是被模数是负数就另当别论了如(-5)%(-2)=-1
/,整数除和小数除是有区别的,整数之间做除法,只保留整数部分而舍弃小数部分。3510/1000*1000=3000

为了避免将比较运算符==误写成=,有经验的程序员干脆写成if(3==x),将常量放在==前面,这样万一写错,编译器就会报错。

逻辑运算符

1
2
& 或者|无论任何情况,&两边的表达式都会参与计算
&& 或者||当左边为false时,则将不会计算其右边的表达式

XOR异或,只有当^连接的两个布尔表达式的值不相同时,该组合才返回true值。

移位运算符

1
2
3
<<左移
>>右移
>>>无符号右移

右移时,如果最高位是0,左边移空的高位就填入0
如果最高位是1,左边移空的高位就填入1
无符号右移,不管高位是0还是1,左边移空的高位都填入0

移位运算符适用数据类型有byte、short、char、int、long

  1. 对低于int类型的操作数将先自动转换为int类型再移位;
  2. 对于int类型整数移位a>>b,系统先将b对32取模,得到的结果才是真正移位的位数。
  3. 对于long类型整数移位时a>>b,则是先将移位位数对64取模。

移位能为我们实现整数除以或乘以2的n次方的效果,如x>>1的结果和x/2的结果是一样的,x<<2和x*4的结果也是一样的。一个数左移n位,就等于这个数乘以2的n次方,一个数右移n位,就等于这个数除以2的n次方。

注意:移位不会改变变量本身的值

经验分享:不要在一行中编写太复杂的表达式,也就是不要在一行中进行太多的运算,除可读性差之外,还极容易出错。多用括号分成多条语句,括号的优先级是最高的。

程序的流程控制

结构化程序设计角度:

  1. 顺序
  2. 选择
  3. 循环
1
2
对于if x then y,还有一种更简洁的写法
变量 = 布尔表达式?语句1:语句2

求绝对值

1
y= x>0?x:-x

在java中,if和elseif括号中的表达式的结果必须是布尔型的,这一点和c/c++不一样

switch语句判断条件可以接受byte,short,int,char类型,不接受其它类型
case是一旦碰到第一次匹配,如果没有break,就会继续执行。

注意while表达式的括号后一定不要加;

do while语句的结尾处多了一个分号;

数组

数组是多个相同类型数据的组合,实现对这些数据的统一管理。

1
2
int [] x = new int[100];
int x[] = new int[100];

数组的静态初始化

1
2
int a = {1,2,3,4};
int a[] = new int[]{3,4,5};

注意:在java语言中声明数组时,无论用何种方式定义数组,都不能指定其长度。

多维数组,在java中并没有真正的多维数组,只有数组的数组,虽然在应用上很像C语言中的多维数组,但还是有区别的。在C语言中定义一个二维数组,必须是一个x*y二维矩阵块,类似我们通常所见到的棋盘。

Java中多维数组不一定是规则矩阵形式


1
int [][] xx = {{3,2,7},{1,5},{6}}

与一维数组一样,在声明多维数组时不能指定其长度。

面向对象

什么是面向对象,这是一个相对概念,是相对面向过程而言的。
要理解什么是幸福,要先理解什么是痛苦一样的道理。

面向过程

Windows窗口–>结构体
HideWindow、MoveWindow,接收参数
谓语与宾语的关系,程序的重心集中在方法(谓语)上

面向对象

Windows窗口,主体,有属性,
有动作(方法,Hide,Move…)
主语与谓语的关系,程序的重点集中在主体/对象(主语)上

封装性,用类封装了数据和方法
真正能体现面向对象的强大优势的地方,是在面向对象的继承与多态性方面
封装性是面向对象的根源和最根本的属性

封装:Encapsulation
继承:Inheritance
多态:Polymorphism

面向对象的编程思想力图使在计算机语言中对事物的描述与现实世界中该事物的本来面目尽可能地一致,类和对象就是面向对象方法的核心概念。类时对某一类事物的描述,是抽象的、概念上的定义,对象是实际存在的该类事物的个体,因而也成实例(Instance)。

面向对象程序设计的重点是类的设计。

类的属性:类的成员变量
类的方法:类的成员方法
一个类中的方法可以直接访问同类中的任何成员(包括成员变量和成员方法)

注意:方法中的变量若与类成员变量同名,则该方法中对这个变量名的访问时局部变量而非成员变量。

对象的引用句柄是在栈中分配的一个变量,对象本身是在堆中分配的,原理同之前讲过的数组一样。

当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值,除了基本数据类型之外的变量类型都是引用类型。

1
2
3
4
5
6
7
byteshortint0
long:0L
float:0.0F
double:0.0D
char'\u0000'(表示为空)
booleanfalse
All reference type:null

对象的比较

==用于比较两个变量的值是否相等
equals()方法用于比较两个对象的内容是否一致

equls方法是String类的一个成员方法,用于比较两个引用变量所指向的对象的内容是否相等,就像比较两个人的长相是否一样。

匿名对象

创建完对象,在调用该对象的方法时,也可以不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。

1
2
3
4
5
Person p1 = new Person();
p1.show();

// 这句代码执行完,这个对象也就变成了垃圾
new Person().show();

使用匿名对象的两种情况:
A、如果对一个对象只需要进行一次方法调用
B、将匿名对象作为实参传递给一个方法调用

类的封装

如果外面的程序可以随意修改一个类的成员变量,会造成不可预料的程序错误,就像一个人的身高,不能被外部随意修改,只能通过各种摄取营养的方法去修改这个属性。

为了实现良好的封装性,通常将类的成员变量声明为private,再通过public的方法来对这个变量进行访问。

一个类通常就是一个小的模块,模块设计追求强内聚(许多功能尽量在类的内部独立完成,不让外面干预),弱耦合(提供给外部尽量少的方法调用)。

构造方法

  1. 它具有与类相同的名称
  2. 它不含返回值
  3. 它不能在方法中用return语句返回一个值

构造方法在程序设计中非常有用,它可以为类的成员变量进行初始化工作。
每一个人一出生就必须先洗澡。

注意:在构造方法里不含返回值的概念是不同于void的,对于public void这样的写法就不再是构造方法,而变成了普通方法。

构造方法的重载
new 类名(参数列表)

在执行构造方法中的代码之前,进行属性的显式初始化,也就是执行在定义成员变量时就对其进行赋值的语句。

在java的每个类里都至少有一个构造方法,如果没有定义,系统会自动为这个类产生一个默认的构造方法,没有参数,方法体中也没有任何代码,即什么也不做。

一旦定义了构造方法,系统就不再产生默认的构造方法。

经验:只要定义有参数的构造方法,都最好再定义一个无参数的构造方法。构造方法一般都是public的,因为它们在对象产生时,会被系统自动调用。

每个成员方法内部,都有一个this引用变量,指向调用这个方法的对象。

对于一个方法来说,只要是对象,它就可以调用,它根本就不区分是不是自己所属的那个对象。

this应用场景:
A、想通过构造方法将外部传入的参数赋值给类成员变量,构造方法的形式参数名称与类的成员变量名相同;

B、假设有一个容器类和一个部件类,在容器类的某个方法中要创建部件类的实例对象,而部件类的构造方法要接收一个代表其所在容器的参数。

C、我们可以在一个构造方法里调用其他重载的构造方法,不是用构造方法名,而是用this(参数列表)的形式,根据其中的参数列表,选择相应的构造方法。

GC有关的知识

皮之不存毛将焉附,如果对象都不存在,又怎么能够调用它的方法,无论是构造方法被调用,还是析构方法被调用,对象都在内存中存在。

Java的finalize()方法的作用类似c++中的析构方法,finalize()方法是在对象被当成垃圾从内存中释放前调用,而不是在对象变成垃圾前调用,垃圾回收器的启用不由程序员控制,业务规律可循,并不会一产生垃圾,它就被唤起,甚至有可能到程序终止,它都没有启动的机会。

System.gc(),强制启动垃圾回收器来回收垃圾。

读代码时,不是专盯代码本身,而是要看内存状态。

方法的参数传递

基本数据类型的参数传递

基本类型的变量作为实参传递,并不能改变这个变量的值。

引用数据类型的参数传递

一个对象可以有多个句柄(名称/引用)

Java语言在给被调用方法的参数赋值时,只采用传值的方式。所以,基本类型数据传递的是该数据的值本身,引用类型数据传递的也是这个变量的值本身,即对象的引用(句柄),而非对象本身,通过方法调用,可以改变对象的内容,但是对象的引用是不能改变的。对于数组,也属于引用类型。

static关键字

static 变量

有时候,我们希望无论是否产生了对象,或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份,例如所有的中国人都有国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

静态变量在某种程度上与其他语言的全局变量相类似,如果不是私有的就可以在类的外部进行访问,此时不需要产生类的实例对象,只需要类名就可以引用。对于静态成员变量,我们叫类属性(class attributes)

用static标识符修饰的变量,它们在类被载入时创建,只要类存在,static变量就存在。

static 方法

对于静态成员方法,我们叫类方法(class method),采用static关键字说明类的属性和方法不属于类的某个实例对象。

注意:

  1. 在静态方法里只能直接调用同类中其他的静态成员(包括变量和方法),而不能直接访问类中的非静态成员。这是因为,对于非静态的方法和变量,需要先创建类的实例对象后才可使用。
  2. 静态方法不能以任何方式引用this和super关键字,道理同1
  3. main()方法是静态的,因此jvm在执行main方法时不创建main方法所在的类的实例对象,因而在main方法中,不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员。

静态代码块

static block,当类被载入时,静态代码块被执行,且只被执行一次,静态块经常用来进行类属性的初始化。尽管产生了类的多个实例对象,但其中的静态代码块只被执行一次。

当一个程序用到了其他的类,才会去装载那个类。

类是在第一次被使用的时候才被装载,而不是在程序启动时就装载程序中所有可能要用到的类。

单态设计模式

设计模式是在大量的实践总结和理论化之后优选的代码结构、编程风格以及解决问题的思考方式。设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱,免得自己再去思考和摸索。

了解和掌握设计模式,这也是java开发者提高自身素质的一个很好选择。

单态设计模式:在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

  1. 将类的构造方法的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类的内部仍可以产生该类的对象。

理解main方法的语法

内部类

在一个类内部定义类,这就是嵌套类(nested classes),也叫内部类。
嵌套类可以直接访问嵌套它的类的成员,包括private成员,但是嵌套类的成员却不能被嵌套它的类直接访问。

在类中直接定义的嵌套类的使用范围,仅限于这个类的内部,A类中定义了一个B类,那么B为A所知,却不被A的外面所知。内部类可以声明为private或protected。

在内部类对象保存了一个对外部类对象的引用,当内部类的成员方法中访问某一变量时,如果在该方法和内部类中都没有定义过这个变量,调用就会被传递给内部类中保存的那个外部类对象的引用,通过那个外部类对象的引用去调用这个变量,在内部类中调用外部类的方法也是一样的道理。

应用场景:
当一个类中的程序代码要用到另一个类的实例对象,而另一个类中的程序代码又要访问第一个类中的成员,将另一个类做成第一个类的内部类,程序代码就要容易编写得多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Outer {

int out_i = 100;

void test(){
Inner in = new Inner();
in.display();
}

class Inner{
void display()
{
System.out.println("display out_i = " + out_i);
}
}

}

如果用static修饰一个内部类,这个类就相当于是一个外部定义的类。
非static内部类中,成员是不能声明为static的。


内部类也可以通过创建对象从外部类之外被调用,只要将内部类声明为public即可。

内部类Inner被声明为public,在外部就可以创建其外部类Outer的实例对象,再通过Outer类的实例对象创建Inner类的实例对象,就可以使用Inner类的实例对象来调用内部类Inner中的方法了。

嵌套类也可以在程序块的范围内定义。如,在方法中,或甚至在for循环体内部等。

在方法中定义的内部类只能访问方法中的final类型的局部变量,因为用final定义的局部变量相当于是一个常量,它的生命周期超出方法运行的生命周期。

类的继承

关键字:extends

  1. java只支持单继承,不允许多重继承,即:一个子类只能有一个父类
  2. 一个类可以被多个类继承
  3. 可以有多层继承
  4. 子类继承父类所有的成员变量和成员方法,但不继承父类的构造方法。在子类的构造方法中可使用语句super(参数列表)调用父类的构造方法。
  5. 如果子类的构造方法中没有显式地调用父类的构造方法,也没有使用this关键字调用重载的其它构造方法,则在产生子类的实例对象时,系统默认调用父类无参数的构造方法。
  6. 如果子类构造方法中没有显式地调用父类构造方法,而父类中又没有无参数的构造方法,则编译出错。经验:我们在定义类时,只要定义了有参数的构造方法,通常都还需要定义一个无参数的构造方法。

子类对象实例化过程

  1. 分配成员变量的存储空间并进行默认的初始化;
  2. 绑定构造方法参数,就是new Class(实际参数列表)中所传递进的参数赋值给构造方法中的形式参数变量;
  3. 如有this()调用,则调用相应的重载构造方法,被调用的重载构造方法的执行流程结束后,回到当前构造方法,当前构造方法直接跳转到步骤6执行;
  4. 显式或隐式追溯调用父类的构造方法(一直到Object类为止,Object是所有java类的最顶层父类),父类的构造方法又从步骤2开始对父类执行这些流程,父类的构造方法的执行流程结束后,回到当前构造方法,当前构造方法继续往下执行。
  5. 进行实例变量的显式初始化操作,也就是执行在定义成员变量时就对其进行赋值的语句。
  6. 执行当前构造方法的方法体中的程序代码。

注意:this()方法调用语句与this.school=school的区别,前者指调用其他的构造方法,后者是一个普通的赋值语句。

为什么?

  1. 为什么super(…)和this(…)调用语句不能同时在一个构造方法中出现?
  2. 为什么super(…)或this(…)调用语句只能作为构造方法的第一句出现?

覆盖父类的方法

覆盖方法必须和被覆盖方法具有相同的方法名称、参数列表和返回值类型
覆盖方法时,不能使用比父类中被覆盖的方法更严格的访问权限,如父类的方法时public,子类的方法就不能是private。

final关键字

  1. 在java中声明类、属性和方法时,可使用关键字final来修饰。
  2. final标记的类不能被继承。
  3. final标记的方法不能被子类重写;
  4. final标记的变量(成员变量或局部变量)即成为常量(这个常量也只能在这个类的内部使用,不能在类的外部直接使用,当用public static final共同标记常量时,这个常量就成了全局的常量,并且只能在定义时赋值,java中的全局常量也放在一个类中定义),只能赋值一次。final标记的成员变量必须在声明的同时或在该类的构造方法中显式赋值,然后才能使用。
  5. 方法中定义的内置类只能访问该方法内的final类型的局部变量,用final定义的局部变量相当于是一个常量,它的生命周期超出方法运行的生命周期。将一个形参定义成final也是可以的,这就限定了我们在方法中修改形式参数的值。

抽象类

Java中可以定义一些不含方法体的方法,它的方法体的实现交给该类的子类根据自己的情况去实现,这样的方法就是抽象方法,包含抽象方法的类就叫抽象类,一个抽象类中可以有一个或多个抽象方法。任何带有抽象方法的类都必须声明为抽象类。

修饰符:abstract
抽象类不能被实例化
抽象方法只需声明,而不需实现。

抽象类的子类必须覆盖所有的抽象方法后才能被实例化,否则这个子类还是个抽象类。

abstract 返回值类型 抽象方法(参数列表)

接口

如果一个抽象类中的所有方法都是抽象的,就可以将这个类用另外一种方式来定义,也就是接口定义。

接口是抽象方法和常量值的定义的集合,从本质上讲,接口是一种特殊的抽象类,这种类中只包含常量和方法的定义,而没有变量和方法的实现。

即使没有显式地将其中的成员用public关键字标识,但这些成员都是public访问类型的。

接口里的变量默认是用public static final标识的,所以,接口中定义的变量就是全局静态常量。

  1. 可以用extends关键字去继承一个已有的接口
  2. 也可以定义一个类,用implements关键字去实现一个接口中的所有方法
  3. 还可以去定义一个抽象类,用implements关键字去实现一个接口中定义的部分方法

在java中,设计接口的目的是为了让类不必受限于单一继承的关系,而可以灵活地同时继承一些共有的特性,从而达到多重继承的目的,而且避免了C++中多重继承的复杂关系所产生的问题。多重继承的危险性在于一个类有可能继承了同一个方法的不同实现,对接口来讲绝不会发生这种情况,因为接口没有任何实现。

一个类可以在继承一个父类的同时,实现一个或多个接口,extends关键字必须位于implements关键字之前。

  1. 实现一个接口就是要实现该接口的所有方法(抽象类除外)
  2. 接口中的方法都是抽象的
  3. 多个无关的类可以实现同一个接口
  4. 一个类可以实现多个无关的接口

调用者和被调用者必须共同遵守某一限定,调用者按照这个限定进行方法调用,被调用者按照这个限定进行方法实现,在面向对象的编程语言中,这种限定就是通过接口类来表示的,主板和各种PCI卡就是按照PCI接口进行约定的。

java编译器并不能根据一个类中有哪些方法,就知道它是某个类的子类的,编译器只能从extends和implements关键字上来了解。

对象的多态性

对象的类型转换

  1. 子类转换成父类
  2. 父类转换成子类
    强制转换:目的是让编译器进行语法检查时开点后门,放你过关,强制类型转换并不是要对内存中的对象大动手术,不是要将男人变成女人。

强制类型转换的前提是程序员提前就知道要转换的父类引用类型对象的本来面目确实是子类类型的。

可以用instanceof判断是否一个类实现了某个接口,也可以用它来判断一个实例对象是否属于一个类。

Object类

Object类时java类层中的最高层类,是所有类的超类,java中任何一个类都是它的子类,由于所有类都是object衍生出来的,所以object的方法适用于所有类。

Object中有一个equals方法,用于比较两个对象是否相等,默认值为false,为了确保准确,自定义类中必须覆盖Object类的equals方法。

多态性

  1. 应用程序不必为每一个派生类(子类)编写功能调用,只需要对抽象基类进行处理即可。
  2. 派生类的功能可以被基类的方法或引用变量调用,这叫后向兼容,可以提高程序的可扩充性和可维护性。

匿名内部类

内部类可以声明是抽象类或是一个接口,它可以被另外一个内部类来继承或实现,内部类可以继承外部类,也可以用final关键字修饰。

异常

异常定义了程序中遇到的非致命的错误,而不是编译时的语法错误,如程序打开一个不存在的文件,网络连接中断,操作数越界,装载一个不存在的类等…

当try代码块中的程序发生了异常,系统将这个异常发生的代码行号,类别等信息封装到一个对象中,并将这个对象传递给catch代码块。

throws关键字

定义方法时用throws关键字声明了它有可能发生异常,调用者就必须使用try…catch语句进行处理,这叫防患于未然。

在a调用方法中不处理,继续throws,一直到main方法
java中一个方法时可以被声明成抛出多个异常的。

Exception类是所有异常类的父类,除了ArithmeticException、NullPointerException、ArrayIndexOutOfBoundsException等系统异常外,我们也可以定义自己的异常类。

java是通过throw关键字抛出异常对象的
throw 异常对象;

在一个方法内使用throw关键字抛出了异常对象,如果该方法内部没有用try…catch语句对这个抛出的异常进行处理,则此方法应声明抛出异常,而由该方法的调用者负责处理。

我们可以在一个方法中使用throw、try…catch语句来实现程序的跳转

finally关键字

finally语句中的代码不管异常是否被捕获总是要被执行的。
finally中的代码块不能被执行的唯一情况是:在被保护代码块中执行了System.exit()

注意:

  1. 一个方法被覆盖时,覆盖它的方法必须抛出相同的异常或异常的子类;
  2. 如果父类抛出多个异常,那么重写(覆盖)方法必须抛出那些异常的一个子集,也就是说,不能抛出新的异常。

java没有goto语句,它保留goto关键字只是为了让程序员不要搞混。java利用带标号的break和continue语句来取代goto。java中严格定义的异常处理机制使goto没有再存在的必要,取消这种随意跳转的语句有利于优化代码以及保持系统的健壮性和安全性。

java异常强制我们去考虑程序的强健性和安全性

java通过引入package机制,提供类的多层类命名空间。

位于包中的每个类的完整名称都应该是包名与类名的组合。
同一个包中的类相互访问,不用指定包名。

同是中国的两个城市,使用时非要加个中国前缀,也是行得通的,但让人听起来就有点像汉奸了

如果从外部访问一个包中的类,必须使用类的完整名称。
虚拟机在装载带有包名的类时,会先找到classpath环境变量指定的目录,再在这些目录中,按照与包名层次相对应的目录结构去查找class文件。

在编译时,让javac来生成与包名层次相对应的目录结构,而不必手工去创建。

javac -d . Test.java

. 代表当前目录

如果没有-d选项,.class文件存放在当前工作目录

位于在包中的类,在文件系统中的存放位置,必须有与包名层次相对应的目录结构。在package语句中,用.来指明包的层次。

虚拟机在装载带有包名的类时,会先找到classpath环境变量指定的目录,再在这些目录中,按照与包名层次相对应的目录结构去查找class文件。

  1. 即使java/class文件名相同,但其中包含的类的完整名称却不一定相同。
  2. 同一个包中的类不必位于同样的目录,因为java是通过classpath去寻找顶层包名。

包名必须在程序中通过package语句指定,而不是靠目录结构来指定,先要有了包名后,才需要相应的目录结构。

包声明放在源文件的最前面,每个源文件只能声明一个包。

在实际应用中,虽然编译了一个修改过的java源文件,但运行可能是某个旧的class文件,特别是旧的class文件所在的目录再classpath环境变量中的位置,位于新的class文件所在的目录的前面,问题就更加隐蔽了。

当我们在编译过程中遇到了问题,有时并不是程序本身所带来的问题,需要我们放眼全局,思路更加开阔一些,从多个方面去思考和解决问题。

import

父包和子包之间,能从语意上表示某种血缘和亲近关系,但父包和子包在使用上没有任何关系,如父包中的类调用子包中的类,必须引用子包的全名,而不能省略父包名部分。

import一个包中所有的类,并不会import这个包中的子包中的类,如果程序中用到了子包的类,需要再次对子包作单独引入。

jdk中的常用包

  1. java.lang
    包含一些java语言的核心类,如String、Math、Integer、System和Thread,提供常用功能;

java1.2以后的版本中,java.lang这个包会自动被导入,对于其中的类,不需要使用import语句来做导入,如System类

  1. java.io
    包含能提供多种输入/输出功能的类

  2. java.util
    包含一些实用工具类,如定义系统特性、使用与日期日历相关的方法。

  3. java.net
    包含执行与网络相关的操作的类

访问控制

访问修饰符共有4个:default、public、protected、private

类成员

private
不能在方法体内声明的变量前加private修饰符

default 默认访问控制:default、friendly、package
对于默认访问控制成员,可以被这个包中的其它类访问。如果一个子类与父类位于不同的包中,子类也不能访问父类中的默认访问控制成员。

protect:既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。

public:所有类,不管访问类与被访问类是否在同一个包中。

只有两种访问权限
即:public和默认

父类不能是private和protected,否则子类无法继承
public修饰的类能被所有的类访问,
默认修饰的类,只能被同一包中的所有类访问。

java命名习惯

  1. 包名中的字母一律小写,如xxyyzz
  2. 类名、接口名应当使用名词,每个单词的首字母大写,如XxYyZz
  3. 方法名,第一个单词小写,后面每个单词的首字母大写,如xxYyZz
  4. 变量名,第一个单词小写,后面每个单词的首字母大写,如xxYyZz
  5. 常量名中的每一个字母一律大写,如XXXYYYZZZ

使用jar文件

我们用的jdk中的包与类主要在jdk的安装目录的jre\lib\rt.jara文件中,由于java虚拟机会自动找到这个jar包,所以我们在使用这个jar包的类时,无需再用classpath来指向它们的位置。

jar: java archive file

jar文件是一种压缩文件,与常见的zip压缩文件格式兼容,习惯上称之为jar包,我们将开发的类压缩到jar文件中,以jar包的方式提供给别人使用。只要别人的classpath环境变量的设置中包含这个jar文件,java虚拟机就能自动在内存中解压这个jar文件,把这个jar文件当做一个目录,在这个jar文件中去寻找所需要的类及包名所对应的目录结构。

jar 命令详解

jar命令是随jdk自动安装的,存放在jdk安装目录下的bin目录中。

jar命令可以用来对大量的类进行压缩,然后存为jar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Usage: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
Options:
-c create new archive
-t list table of contents for archive
-x extract named (or all) files from archive
-u update existing archive
-v generate verbose output on standard output
-f specify archive file name
-m include manifest information from specified manifest file
-n perform Pack200 normalization after creating a new archive
-e specify application entry point for stand-alone application
bundled into an executable jar file
-0 store only; use no ZIP compression
-P preserve leading '/' (absolute path) and ".." (parent directory) components from file names
-M do not create a manifest file for the entries
-i generate index information for the specified jar files
-C change to the specified directory and include the following file
If any file is a directory then it is processed recursively.
The manifest file name, the archive file name and the entry point name are
specified in the same order as the 'm', 'f' and 'e' flags.

Example 1: to archive two class files into an archive called classes.jar:
jar cvf classes.jar Foo.class Bar.class
Example 2: use an existing manifest file 'mymanifest' and archive all the
files in the foo/ directory into 'classes.jar':
jar cvfm classes.jar mymanifest -C foo/ .

注意:使用jar压缩文件夹时,在生成的jar文件中会保留在jar命令中所出现的路径名

多线程

概念

在多任务系统中,每个独立执行的程序称为进程,也就是”正在进行的程序”

每个进程都有独立的代码和数据空间(进程上下文)

一个线程就是一个程序内部的一条执行线索

当程序启动运行时,就自动产生了一个线程,主方法main就是在这个线程上运行的,当不再产生线程时,程序就是单线程的。如果要一个程序实现多段代码同时交替运行,就需产生多个线程,并指定每个线程上所要运行的程序代码段。

创建多线程有两种方法,继承Thread类和实现Runnable接口。

用Thread类创建线程

Java的线程时通过java.lang.Thread类来控制的,一个Thread类的对象代表一个线程,而且只能代表一个线程,通过Thread类和它定义的对象,我们可以获得当前线程对象、获取某一线程的名称,可以实现控制线程暂停一段时间等功能。

  1. 要将一段代码在一个新的线程上运行,该代码应该在一个类的run方法中,并且run方法所在的类是Thread类的子类。倒过来说,要实现多线程,必须编写一个继承了Thread类的子类,子类要覆盖Thread类中的run方法,在子类的run方法中调用想在新线程上运行的程序代码。

  2. 启动一个新的线程,不是直接调用Thread子类对象的run方法,而是调用Thread子类对象的start(从Thread类中继承的)方法,Thread类对象的start方法将产生一个新的线程,并在该线程上运行该Thread类对象中的run方法,根据面向对象的多态性,在该线程上实际运行的是Thread子类(也就是我们编写的那个类)对象中的run方法。

  3. 由于线程的代码在run方法中,那么该方法执行完以后,线程也就相应的结束了,因而可以通过控制run方法中的循环条件来控制线程的终止。

Thread类有许多构造方法,通过Thread()构造方法创建的,线程将调用线程对象的run()方法作为其运行代码。如果Thread类的子类没有覆盖run方法,则程序会调用Thread类中的run方法,而该方法什么也不做,所以新的线程刚一产生就结束了。

直接在程序中写 new Thread().start(),新的线程将直接调用Thread类中的run()方法,效果同上。

综上,使用Thread()构造方法,适用于覆盖了run方法的Thread子类创建线程对象的情况。

使用Runnable接口创建多线程

Thread(Runnable target)构造方法
Runnable接口类,该接口只有一个run()方法,当使用Thread(Runnable target)方法创建线程对象时,需要为该方法传递一个实现了Runnable接口的类对象,这样创建的线程将调用那个实现了Runnable接口的类对象中的run()方法作为其运行代码,而不再调用Thread类中的run方法了。

一个线程对象只能启动一个线程,无论你调用多少遍start()方法,结果都只有一个线程。

创建多个Thread类子类的对象,就等于创建了四个资源,每个线程在独立地处理各自的资源。

经验:一个资源(一个Thread类子类或实现了Runnable接口的类的对象),创建多个线程去处理同一个资源,并且每个线程上所允许的是相同的程序代码。

如:Windows上可以启动多个记事本程序,也就是多个进程使用的是同一个记事本程序代码。

对比

实现Runnable接口相对于继承Thread类来说,有下述好处:

  1. 适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码、数据有效分离,较好地体现了面向对象的设计思想。

  2. 可以避免由于java的单继承性带来的局限。当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时又两个父类,所以不能用继承Thread类的方式,只能采用实现Runnable接口的方式。

  3. 有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自于同一个类的实例时,即称它们共享相同的代码,多个线程可以操作相同的数据,与它们的代码无关。当共享访问相同的对象时,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造方法实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。

后台线程

对java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程只有后台线程运行,这个进程就会结束。前台线程时相对后台线程而言的,如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了后台线程。

join联合线程

Thread类的join方法,作用是把a线程合并到调用a.join()语句的线程中。除了无参数的join方法外,还有两个带参数的join方法,分别是join(long millis)和join(long millis, int nanos),它们的作用是指定合并时间,前者精确到毫秒,后者精确到纳秒,意思是两个线程合并指定的时间后,又开始分离,回到合并前的状态。

多线程实践

  1. 网络聊天程序
    发送过程与接收过程在两个不同的线程上运行,彼此之间没有任何约束

  2. 耗时数据处理取消机制
    创建一个新的线程,该线程与用户交互,接收用户的输入,当接收到用户的停止命令时,新线程将主线程的循环条件Flag设置为假,即通知主线程在下次检查循环条件时结束复制过程。

1
2
3
4
boolean bFlag =true;
while(bFlag){
//复制程序
}
  1. www服务器
    为每一个来访者创建一个线程

多线程同步

sleep(long millis)
线程的睡眠是可以被打断的,通过Thread.interrupt(),线程的睡眠被打断后进入Runnable状态。

线程安全,类的同一个实例的方法在多个线程被调用,是否会出现意外。

一、同步代码块
代码块的原子性,好比一座独木桥,任一时刻,只能有一个人在桥上行走,程序中不能有多个线程同时在这两句代码之间执行,这就是线程同步。

将这些需要具有原子性的代码,放入synchronized语句中

1
2
// object 可以是任意的一个对象
synchronized(object){代码段}

任何类型的对象都有一个标志位,该标志位具有0、1两种状态,其开始状态为1,当执行synchronized(object)语句后,object对象的标志位变为0状态,知道执行完整个synchronized语句中的代码块后又回到1状态。

一个线程执行到synchronized语句处,先检查object对象(监视器)的标志位(锁旗标),如果为0状态,表明已经有另外的线程的执行状态正在有关的同步代码块中,这个线程将暂时阻塞(加入到一个与该对象的锁旗标相关联的等待线程池中),让出cpu资源,直到另外的线程执行完有关的同步代码块,将object对象的标志位恢复到1状态,这个阻塞就被取消,线程能够继续往下执行,并将object对象的标志位变为0状态,防止其它线程再进入有关的同步代码块中。如果有多个线程因等待同一对象的标志位而处于阻塞状态时,当对象的标志位恢复到1状态时,只会有一个线程能够继续运行,其它线程仍然处于阻塞等待状态。

若干个不同的代码块也可以实现相互之间的同步,只要各synchronized(object)语句中的object完全是同一个对象就可以。

一个刚锁定了监视器的线程在监视器被解锁后可以再次进入并锁定同一监视器,好比篮球运动员的篮球出手后可以再次去抢回来一样。

当在同步块中遇到break语句或抛出异常时,线程也会释放该锁旗标。

当cpu进入了一段同步代码块中执行,cpu是可以切换到其他线程的,只是在准备执行其他线程的代码时,发现其它线程处于阻塞状态,cpu又会回到先前的线程上。这个过程就类似于幸运之神刚一光顾其他有关线程,没想到吃了个闭门羹,便又离开了。

同步是以牺牲程序的性能为代价的,因为i系统要不停地对同步监视器进行检查,需要更多的开销。

除了可以对代码块进行同步外,也可以对方法实现同步,只要在需要同步的方法定义前加上synchronized关键字即可。

同步方法所用的监视器对象就是this对象

老手就是在大量的实践中,犯下众多错误,然后经过反复调试、观察、比较,最后在总结、积累经验的过程中成长起来的。在计算机编程过程中,我们有时候会因为自己知识的不全面而作出错误的结论。

死锁问题

两个线程对两个同步对象具有循环依赖时,就会出现死锁,例如一个线程进入对象X的监视器,而另一个对象进入了对象Y的监视器,这时进入X对象监视器的线程如果还试图进入Y对象的监视器就会被阻隔,接着进入Y对象监视器的线程如果试图进入X对象的监视器也会被阻隔,这样两个线程都处于挂起状态。程序发生死锁后最明显的特征就是程序的运行处于停滞不前状态。

两个人在吃饭,甲拿到了一根筷子和一把刀子,乙拿到了一把叉子和一根筷子,他们都无法吃到饭。
甲:你先给我筷子,我再给你刀子
乙:你先给我刀子,我才给你筷子
……

线程间的通信

java是通过object类的wait、notify、notifyall这三个方法来实现线程间的通信。所有的类都是从Object继承的,因此在任何类中都可以直接使用这些方法。

wait:告诉当前线程放弃监视器并进入睡眠状态,直到其他线程进入同一监视器并调用notify为止;
notify:唤醒同一对象监视器中调用wait的第一个线程,类似于饭馆有一个空位后通知所有等候就餐的顾客中的第一位可以入座的情况;
notifyAll:唤醒同一个对象监视器中调用wait的所有线程,只有最高优先级的线程首先被唤醒并执行。

上述三个方法只能在synchronized方法中调用,即:无论线程调用一个对象的wait还是notify方法,该线程必须先得到该对象的锁旗标,这样,notify只能唤醒同一个对象监视器中调用wait的线程,使用多个对象监视器,我们就可以分组有多个wait、notify的情况,同组里的wait只能被同组的notify唤醒。

一个线程的等待和唤醒过程可以用下图表示:

线程生命周期

线程产生和消亡的整个过程,如下图所示:

一个线程的产生式从我们调用了start方法开始进入Runnable状态,即可以被调度运行状态,并没有真正开始运行,调度器可以将CPU分配给它,真正运行其中的程序代码。线程在运行过程中,有以下几个可能的去向:

  1. 没有遇到任何阻隔,运行完成直接结束,也就是run()方法执行完毕;
  2. 调度器将CPU分配给其他线程,这个线程又变为Runnable状态;
  3. 请求锁旗标,却得不到,这时候它要等待对象的锁旗标,得到锁旗标后又会进入Runnable状态开始运行;
  4. 遇到wait方法,它会被放入等待池中继续等待,直到有notify()或interrupt()方法执行,它才会被唤醒或打断,开始等待对象锁旗标,等到锁旗标后进入Runnable状态继续执行。

控制线程生命周期的方法有suspend、resume和stop方法,但不推荐使用。

不推荐使用suspend和resume的原因如下:

  1. 会导致死锁的发生;
  2. 它允许一个线程通过直接控制另外一个线程乙的代码来直接控制那个线程乙。

不推荐使用stop的原因如下:
虽然stop能够避免死锁的发生,但是带来了另外的不足,如果一个线程正在操作共享数据段,操作过程中没有完成就stop了的话,就会导致数据的不完整性。

经验:推荐使用控制run方法中循环条件的方式来结束一个线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ThreadLife {

public static void main(String[] args) {

ThreadLifeTest threadLifeTest = new ThreadLifeTest();
new Thread(threadLifeTest).start();

for(int i=0; i<10; i++){
if(5==i){
threadLifeTest.stopMe();
}
System.out.println("main thread is running");
}
}
}

class ThreadLifeTest implements Runnable{

private boolean bFlag = true;

public void stopMe(){
bFlag = false;
}

public void run(){

while(bFlag){
System.out.println("ThreadLifeTest.run()");
}
}
}

注意:调用了目标线程的stopMe方法之后,cpu不一定会马上切换到目标线程,目标线程也就不一定会马上终止。稍后切换到目标线程之后,终止了while循环,run方法结束,目标线程随之结束。

Java API

API Application Programming Interface

学习编程语言与学汉语

  1. 语法

  2. 成语
    学习编程语言,掌握了大量API,就像学习汉语时掌握了大量的成语一样,我们在处理某些问题时将会轻而易举,同样,我们也能从这些API类中学会大师们组织java类的方法,划分系统结构的技巧。

区别:对API来说,完全可以在需要时通过某种方式临时获取,现学现用

  1. 学会写文章的技巧和手法,找到写文章的灵感
    学习编程,也需要掌握分析与解决问题的手法,养成良好的编程习惯与风格,体会出一种编程的感觉。

唐诗宋词/优秀散文
优秀的源代码
一些大学老师从事实际项目开发的经验不太丰富,只能讲些语法和API方面的知识,没有能力帮你分析与讲解编程经验与体会,就像许多小学老师自己都写不出好的作文来,但却可以成为语文老师一样的道理。

专家不是靠书本学出来的,是有了一定的基础后,在工作中再总结、在学习的过程中成长起来的。我们没有必要去了解一门语言中的每个方面和细节,虽然,我们了解的越多,水平似乎就越高,但这都是要以时间和精力为代价的,学习到一定程度后,要适可而止,否则,一辈子都只有疲于学习的份儿了,就完全违背了“学以致用”的初衷。

当你掌握了一门语言的语法特点后,能够看懂一般的程序,在需要时能够参照文档资料看懂以前还没有接触过的某个方面的程序,能够自己写出以恶搞有某种实际应用的小程序,你就算掌握了这门语言,剩下的就是你在工作中如何去积累经验的问题了。

会写一个程序的标准,是理解编程思想,再用自己的想法去独立写下来,对程序中的每个细节都是真正明白的。

String类和StringBuffer类

一个字符串就是一连串的字符,Java定义了String和StringBuffer两个类来封装对字符串的各种操作,都被放到了java.lang包中,不需要import java.lang这个语句导入该包就可以直接使用它们。

String类用于比较两个字符串、查找和抽取串中的字符或子串、字符串与其他类型之间的相互转换等。String类对象的内容一旦被初始化就不能再改变。

StringBuffer类用于内容可以改变的字符串,可以将其它各种类型的数据增加,插入到字符串中,也可以翻转字符串中原来的内容。一旦通过StringBuffer生成了最终想要的字符串,就应该使用StringBuffer.toString方法将其转换成String类,随后,就可以使用String类的各种方法操纵这个字符串了。

Java为字符串提供了特别的连接操作符+,可以把其它各种类型的数据转换成字符串,并前后连接成新的字符串。连接操作符+的功能是通过StringBuffer类和它的append方法实现的。

String x = “a”+4+”c”;

等效于:

1
2
3
4
5
StringBuffer temp = new StringBuffer();
temp.append("a");
temp.append("4");
temp.append("c");
x =temp.toString();

注意:
String s1 = “hello”
String s2 = “hello”
s1和s2是同一个对象。

String s1 = new String(“hello”);
String s2 = new String(“hello”);
s1和s2是两个不同的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ReadLine {

public static void main(String[] args) throws IOException {

byte buf[] = new byte[1024];
String strInfo = null;
int pos = 0;
int ch = 0;

System.out.println("please enter info, input bye for exit");

while(true){

ch = System.in.read();

switch(ch){
case '\r':
break;

case '\n':
strInfo = new String(buf,0,pos);
if("bye".equals(strInfo)){
return;
}
else{
System.out.println(strInfo);
pos=0;
break;
}

default:
buf[pos++]=(byte)ch;
break;
}
}
}
}

String(buf,0,pos)把数组buf里面的值从0到pos取出,用来创建新的String类对象。
equals,比较String类对象的内容是否等于某一字符串常量
equalsIgnoreCase
indexof(int ch)、indexof(int ch,int fromIndex)
indexof(String str)、indexof(String str, int fromIndex)
substring(int beginIndex)、
substring(int beginIndex, int endIndex) 返回beginIndex到endIndex-1的一个字符串

String类中的replace和toUpperCase都不能改变字符串的内容,它们的返回值都是String类,即生成一个新的字符串,而不是改变原来的字符串内容。

基本数据类型的对象包装类

java对数据既提供基本数据的简单类型,也提供了相应的包装类,使用基本简单数据类型,可以改善系统的性能,也能满足大多数应用需求,但基本简单类型不具有对象的特性,不能满足某些特殊的需求。譬如很多类的很多方法参数类型都是object,Integer类来包装整数。包装类对象在进行基本数据类型的类型转换时也特别有用。

8种基本数据类型都有其对应的包装类

1
2
3
4
5
6
7
8
boolean bool= new Boolean(false);
byte bt= new Byte((byte) 't');
char ch = new Character('t');
short st = new Short((short) 18);
int it = new Integer(18);
long lg = new Long(18);
float ft = new Float(1.8f);
double db = new Double(1.8d);

要善于找出包装类的一些共性的地方,达到举一反三,学一通百的效果

要将字符串转换成基本数据类型,几乎都是用Xxx包装类.parseXxx方式实现(一个例外是对于Boolean类,用的是getBoolean)

要将包装类转换成基本数据,几乎都是Xxx包装类对象.xxxValue方式。

集合类

Vector类

Vector类是Java语言提供的一种高级数据结构,可用于保存一些列对象,java不支持动态数组,Vector类提供了一种与动态数组相近的功能。

如果我们不能预先确定要保存的对象的数目,或是需要方便获得某个对象的存放位置,Vector类都是一种不错的选择。

Enumeration接口类

Enumeration是一个接口类,它提供了一种访问各种数据结构中的所有数据的抽象机制,我们要访问各种数据结构对象中的所有元素时,都可以使用同样的方式,调用同样的方法。有了这样的数据结构,就很容易学一通百,以不变应万变。

Collection接口

Iterator接口

我们要取出保存在实现了Collection接口对象中的所有对象,我们也必须通过Collection.iterator方法返回要给Iterator接口对象,Iterator接口的功能与使用同Enumeration接口非常相似。

Java2平台的数据结构类设计人员本可以扩展Enumeration接口,而不用创建Iterator这个新接口。但他们不喜欢Enumeration接口方法冗长的名字,因而创建了Iterator这个新接口,并缩短了方法名长度。

Java中,不能直接用Collection接口类创建对象,而必须用实现了Collection接口的类来创建对象,ArrayList类就是一个实现了Collection接口的类。

不要变成只会照着书做,而不会自学和思考的人,假设有人真的把java api全部背了下来,以后再遇到别的公司开发的java类,不还是一样不会吗?

什么时候用Vector,什么时候用ArrayList,Vector类中的所有方法都是线程同步的,两个线程并发访问Vector对象将是安全的,但只有一个线程访问Vector对象时,因为源程序仍调用了同步方法,需要额外的监视器检查,运行效率要低一些。

ArrayList类中的所有方法是非同步的,程序的效率会高一些。

集合类接口的比较

Collection——对象之间没有指定的顺序,允许重复元素;
Set——对象之间没有指定的顺序,不允许重复元素;
List——对象之间有指定的顺序,允许重复元素。

Hashtable类

Hashtable也是一种高级数据结构,用以快速检索数据。Hashtable不仅可以像Vector一样动态存储一系列的对象,而且对存储的每一个对象(值value)都要安排另一个对象(关键字key)与之相关联。

向Hashtable对象中存储数据,使用的是Hashtable.put(Object key, Object value)方法,从Hashtable中检索数据,使用Hashtable.get(Object key)方法。值和关键字都可以是任何类型的非空的对象。

要想成功地从Hashtable中检索数据,用作关键字的对象必须正确覆盖了Object.hashCode方法和Object.equals方法(检索时,必须比较所用关键字是否与存储在Hashtable中的某个关键字相等,如果两个关键字对象不能正确判断是否相等,检索是不可能正确的)。

Object.hashCode方法返回一个叫散列码的值,这个值是由对象的地址以某种方式转换来的,内容相同的两个对象,既然是两个对象,地址就不可能一样,所以Object.hashCode返回的值也不一样。

要想两个内容相同的Object子类对象的hashcode方法返回一样的散列码,子类必须覆盖Object.hashCode方法。

用于关键字的类,如果它的两个对象用equals方法比较是相同的,那么这两个对象的hashCode方法返回值也要一样,所以我们也要覆盖hashCode方法。

String类,已按关键字类的要求覆盖了这两个方法,如果两个String对象的内容不相等,它们的hashCode的返回值也不相等,如果两个String对象的内容相等,它们的hashCode的返回值也相等。所以我们在实现自己编写的关键字类的hashCode方法时,可以调用这个关键字类的String类型的成员变量的hashCode方法来计算关键字类的hashCode返回值。

注意:StringBuffer类没有按照关键字类的要求覆盖hashCode方法,即使两个StringBuffer类对象的内容相等,但这两个对象的hashCode方法的返回值却不相等。所以不能用StringBuffer作为关键字类。

Properties类

Properties是Hashtable的子类,它增加了将Hashtable对象中的关键字、值对保存到文件和从文件中读取关键字/值对到Hashtable对象中的方法。

如果要用到Properties类的store方法进行存储,每个属性的关键字和值都必须是字符串类型的。直接用setProperty、getProperty方法进行属性的设置与读取。

System类

Java不支持全局方法和变量,java设计者将一些系统相关的方法和变量收集到了一个统一的类——System类,System类中的所有成员都是静态的,可以直接使用System类名做前缀。如System.in , System.out等

System.exit(int status)方法,提前终止虚拟机的运行,对于发生了异常情况下而想终止虚拟机的运行,传递一个非零值作为参数,对于在用户正常操作下,终止虚拟机的运行,传递零值作为参数。

System.CurrentTimeMillis()方法,返回自1970年1月1日0点0分0秒起至今的以毫秒为单位的时间,这是一个long类型的大数值。在计算机内部,只有数值,没有真正的日期类型及其它各种类型,我们平常用到的日期本质上就是一个数值,但通过这个数值,能够推算出其对应的具体日期时间。

getProperties方法与java环境属性
getProperties方法获得当前虚拟机的环境属性。
Windows环境变量,每一个属性都是变量与值成对的形式出现。
同样的道理,java作为一个虚拟的操作系统,它也有自己的环境属性,Properties是Hashtable的子类,正好可以用于存储环境属性中的多个变量/值对格式的数据,getProperties方法返回值是,包含了当前虚拟机的所有环境属性的Properties类型的对象。

1
2
3
4
5
6
7
8
9
10
11
public class TestProperties {

public static void main(String[] args) {
Properties sp = System.getProperties();
Enumeration e = sp.propertyNames();
while(e.hasMoreElements()){
String key = (String)e.nextElement();
System.out.println(key+"="+sp.getProperty(key));
}
}
}

在windows中,很容易增加一个新的环境属性,如何为java虚拟机增加一个新的环境属性呢?

java 命令有一个-D =格式的选项可以设置新的系统环境属性。

1
2
-D<name>=<value>
set a system property

java -DAAA=bbb
java -DAAA=bbb -DCCC=ddd

注意:-D与AAA之间没有空格

Runtime类

Runtime类封装了Java命令本身的运行进程,其中的许多方法与System中的方法相重复。我们不能直接创建Runtime实例,但可以通过静态方法Runtime.getRuntime获得正在运行的Runtime对象的引用。

Exec方法,Java命令运行后,本身是多任务操作系统上的一个进程,在这个进程启动一个新的进程,即执行其他程序时使用exec方法。exec方法返回一个代表子进程的Process类对象,通过这个对象,Java进程可以与子进程交互。

1
2
3
4
5
6
7
8
9
10
public class TestRuntime {

public static void main(String[] args) throws IOException, InterruptedException {

Process p=null;
p = Runtime.getRuntime().exec("notepad.exe");
Thread.sleep(5000);
p.destroy();
}
}

由于程序不能直接创建类Runtime的实例,所以可以保证我们只会产生一个Runtime的实例对象,而不能产生多个实例对象,这种情况就是单态设计模式。

Date与Calendar、DateFormat类

Date类用于表示日期和时间,最简单的构造方法时Date(),它以当前的日期和时间初始化一个Date对象。

由于开始设计Date时没有考虑到国际化,所以后来又设计了两个新的类来解决Date类中的问题,一个是Calendar类,一个是DateFormat类。

Calendar类是一个抽象基类,主要用于完成日期字段之间相互操作的功能,如Calendar.add方法可以实现在某一日期的基础上增加若干天(或年、月、小时、分、秒等日期字段)后的新日期
Calendar.set方法修改日期对象中的年、月、日、小时、分、秒等日期字段的值。
Calendar.getInstance方法可以返回一个Calendar类型(更确切地说是它的某个子类)的对象实例,GregorianCalendar类是JDK目前提供的一个唯一的Calendar子类,Calendar.getInstance方法返回的就是预设了当前时间的GregorianCalendar类对象。

1
2
3
4
5
//Calendar cl = Calendar.getInstance();
Calendar cl = new GregorianCalendar();
System.out.println(""+cl.get(cl.YEAR)+cl.get(cl.MONTH)+cl.get(cl.DAY_OF_MONTH));
cl.add(cl.DAY_OF_YEAR, 315);
System.out.println(""+cl.get(cl.YEAR)+cl.get(cl.MONTH)+cl.get(cl.DAY_OF_MONTH));

虽然Calendar类几乎完全替代了Date类,但在某些情况下,我们仍有可能要用到Date类,譬如,程序中用的另外一个类的方法要求一个Date类型的参数。

我们有时要将Date对象表示的日期用指定的格式输出和将特定格式的日期字符串转换成一个Date对象。

Java.text.DataFormat就是实现这种功能的抽象基类,
Java.text.SimpleDateFormat类是JDK目前提供的一个DateFormat子类,它是一个具体类,使用它就可以完成把Date对象格式化为本地字符串,或者通过语义分析把日期或时间字符串转换成Date对象的功能。

1
2
3
4
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日");
Date d=sdf1.parse("2017-04-09");
System.out.println(sdf2.format(d));

SimpleDateFormat类就相当于一个模板,其中yyyy对应的是年,MM对应的是月,dd对应的是日。详细可参考JDK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Letter	Date or Time Component	Presentation	Examples
G Era designator Text AD
y Year Year 1996; 96
Y Week year Year 2009; 09
M Month in year (context sensitive) Month July; Jul; 07
L Month in year (standalone form) Month July; Jul; 07
w Week in year Number 27
W Week in month Number 2
D Day in year Number 189
d Day in month Number 10
F Day of week in month Number 2
E Day name in week Text Tuesday; Tue
u Day number of week (1 = Monday, ..., 7 = Sunday) Number 1
a Am/pm marker Text PM
H Hour in day (0-23) Number 0
k Hour in day (1-24) Number 24
K Hour in am/pm (0-11) Number 0
h Hour in am/pm (1-12) Number 12
m Minute in hour Number 30
s Second in minute Number 55
S Millisecond Number 978
z Time zone General time zone Pacific Standard Time; PST; GMT-08:00
Z Time zone RFC 822 time zone -0800
X Time zone ISO 8601 time zone -08; -0800; -08:00

Date and Time Pattern Result
"yyyy.MM.dd G 'at' HH:mm:ss z" 2001.07.04 AD at 12:08:56 PDT
"EEE, MMM d, ''yy" Wed, Jul 4, '01
"h:mm a" 12:08 PM
"hh 'o''clock' a, zzzz" 12 o'clock PM, Pacific Daylight Time
"K:mm a, z" 0:08 PM, PDT
"yyyyy.MMMMM.dd GGG hh:mm aaa" 02001.July.04 AD 12:08 PM
"EEE, d MMM yyyy HH:mm:ss Z" Wed, 4 Jul 2001 12:08:56 -0700
"yyMMddHHmmssZ" 010704120856-0700
"yyyy-MM-dd'T'HH:mm:ss.SSSZ" 2001-07-04T12:08:56.235-0700
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX" 2001-07-04T12:08:56.235-07:00
"YYYY-'W'ww-u" 2001-W27-3

Math与Random类

Math类包含了所有用于几何和三角的浮点运算方法,这些方法都是静态的,每个方法的使用都非常简单,一看JDK文档就能明白。

Random类是一个伪随机数产生器,随机数是按照某种算法产生的,一旦用一个初值创建Random对象,就可以得到一系列的随机数,但如果用相同的初值创建Random对象,得到的随机数序列是相同的,也就是说,在程序中我们看到的“随机数”是固定的那些数,起不到随机的作用,针对这个问题,Java设计者们在Random类的Random()构造方法中使用当前时间来初始化Random对象,因为没有任何时刻的时间是相同的,所以就可以减少随机数序列相同的可能性。

学习API的方法

作者写得越多,读者就要花费更多的时间来阅读,如果读者并不能从更多的篇幅中学到更多有价值的知识,那就是在浪费读者的宝贵时间。

最聪明的人是最会利用工具和资源的人,必须要学会查阅文档。

大家根据自己的实际情况,可以提前通读一下JDK文档中大部分类及类中的方法,做到遇到问题时心中有数,也可以暂时不读,只掌握原理,处理过程,解决方法,等到以后有具体的实际需求时,再来查阅JDK文档。

IO/输入输出

Java语言定义了许多类专门负责各种方式的输入输出,这些类都被放在java.io包中。

程序中,键盘被当做输入文件,显示器被当做输出文件。

File类

File类是IO包中唯一代表磁盘文件本身的对象,File类定义了一些与平台无关的方法来操纵文件,通过调用File类提供的各种方法,我们能够创建、删除文件、重命名文件,判断文件的读写权限及是否存在,设置和查询文件的最近修改时间。

在java中,目录也被当做file使用,只是多了一些目录特有的功能——可以用list方法列出目录中的文件名。在Unix下的路径分隔符为/,在Dos下的路径分隔符为\,Java可以正确处理Unix和Dos的路径分隔符,即使我们在Windows环境下使用/作为路径分隔符,Java仍然能够正确处理。

注意:delete方法删除由File对象的路径所表示的磁盘文件或目录。如果删除的对象是目录,该目录中的内容必须为空。

File类不能访问文件的内容,即不能够从文件中读取数据或往文件里写数据,它只能对文件本身的属性进行操作。

RandomAccessFile类

RandomAccessFile类可以说是Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法。RandomAccessFile类支持“随机访问”方式,我们可以跳转到文件的任意位置处读写数据。

该类仅限于操作文件,不能访问其它的IO设备,如网络、内存映像等。

RandomAccessFile可以以只读或读写方式打开文件,具体使用哪种方式取决于我们创建RandomAccessFile类对象的构造方式:

new RandomAccessFile(f,”rw”);// 读写方式
new RandomAccessFile(f,”r”);// 只读方式

注意:当我们的程序需要以读写的方式打开一个文件时,如果这个文件不存在,程序会为你创建。

String.substring(int beginIndex, int endIndex)方法可以用于取出一个字符串的部分子字符串,要注意的一个细节是:子字符串中的第一个字符对应的是原字符串中的脚标为beginIndex处的字符,但最后的字符对应的是原字符串中的脚标为endIndex-1处的字符,而不是endIndex处的字符。

节点流

流的概念:

数据流是一串连续不断的数据的集合,就像水管里的水流,在水管的一端一点一点地供水,而在水管的另一端看到的是一股连续不断的水流。数据写入程序可以是一段一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据。不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的。

我们将IO流类分为两大类:

  1. 节点流类
    程序用于直接操作目标设备所对应的类。

  2. 包装类(处理流类、过滤流类)
    一个间接流类去调用节点流类,以达到更加灵活方便地读写各种类型的数据。

InputStream与OutputStream

程序可以从中连续读取字节的对象叫输入流,用InputStream类完成。
程序能向其中连续写入字节的对象叫输出流,用OutputStream类完成。

InputStream与OutputStream对象是两个抽象类,它们下面有许多子类,包括网络、管道、内存、文件等具体的IO设备,如FileInputStream类对应的就是文件输入流,是一个节点流类。

我们将这些节点流类所对应的IO源和目标成为流节点(Node)。

输入输出类是相对程序而言的,不是代表文件的。

如要将A文件的内容写入B文件中,
程序要对A文件创建一个输入类,对B文件要创建一个输出类。

InputStream定义了Java的输入流模型。该类中的所有方法在遇到错误时都会引发IOException异常,下面是InputStream类中方法的一个简要说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

// 返回当前输入流中可读的字节数
int available()
Returns an estimate of the number of bytes that can be read (or skipped over) from this input stream without blocking by the next invocation of a method for this input stream.

// 关闭,系统释放与这个流相关的资源
void close()
Closes this input stream and releases any system resources associated with the stream.

// 在输入流的当前位置处放上一个标志,允许最多再读入readlimit个字节
void mark(int readlimit)
Marks the current position in this input stream.

// 如果当前流支持mark/reset操作就返回true
boolean markSupported()
Tests if this input stream supports the mark and reset methods.

// 返回下一个输入字节的整型表示,如果返回-1表示遇到流的末尾,结束。
abstract int read()
Reads the next byte of data from the input stream.

// 读入b.length个字节放到b中bin返回实际读入的字节数
int read(byte[] b)
Reads some number of bytes from the input stream and stores them into the buffer array b.

// 把流中的数据读到数组b中从脚标为off开始的len个数组元素中
int read(byte[] b, int off, int len)
Reads up to len bytes of data from the input stream into an array of bytes.

//把输入指针返回到以前所做的标志处
void reset()
Repositions this stream to the position at the time the mark method was last called on this input stream.

// 跳过输入流上的n个字节并返回实际跳过的字节数。
long skip(long n)
Skips over and discards n bytes of data from this input stream.

InputStream是一个抽象类,程序中实际使用的是它的各种子类对象。不是所有的子类都会支持InputStream中定义的某些方法的,如skip,mark,reset等,这些方法支队某些子类有用。

流是操作系统产生的一种资源。当我们在程序中创建了一个IO流对象,同时系统内也会创建了一个叫流的东西,在这种情况下,计算机内存中实际上产生了两个事物,一个是Java程序中的类的实例对象,一个是系统本身产生的某种资源,窗口、Socket等都是这样的情况,Java垃圾回收器只能管理程序中的类的实例对象,没法去管理系统产生的资源,所以程序需要调用close方法,去通知系统释放其自身产生的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 关闭输出流
void close()
Closes this output stream and releases any system resources associated with this stream.

// 彻底完成输出并清空缓冲区
void flush()
Flushes this output stream and forces any buffered output bytes to be written out.

//将整个字节数组写到输出流中
void write(byte[] b)
Writes b.length bytes from the specified byte array to this output stream.

// 将字节数组b中的从off开始的len个字节写到输出流
void write(byte[] b, int off, int len)
Writes len bytes from the specified byte array starting at offset off to this output stream.

// 将一个字节写到输出流,这里的参数是int类型,它允许write使用表达式而不用强制转换成byte类型。
abstract void write(int b)
Writes the specified byte to this output stream.

计算机访问外部设备,要比直接访问内存慢得多。使用内存缓冲区有两个方面的好处:

  1. 提高cpu的使用率
  2. write并没有马上真正写入到外设,我们还有机会回滚部分写入的数据。

C语言默认情况下就会使用缓冲区,而在java中,有的类使用率缓冲区,有的类没有使用缓冲区,我们还可以在程序中使用专门的包装类来实现自己的缓冲区。

flush方法就是用于即使在缓冲区没有满的情况下,也将缓冲区的内容强制写入到外设,习惯上称这个过程为刷新。它只对那些使用缓冲区的OutputStream子类有效。如果我们调用了close方法,系统在关闭这个流之前,也会将缓冲区的内容刷新到硬盘文件的。

读者花更多的时间去开阔自己的知识面和思维,了解更多的原理,而不是花大量时间去死记硬背某些细节和术语,特别是一个类中的每个方法名的具体拼写,具体的参数形式,java中有哪些关键字等这些死板的东西,只要有个印象就足够了。

FileInputStream与FileOutputStream

这两个流节点用来操作磁盘文件,在创建一个FileInputStream对象时通过构造方法指定文件的路径和名字,当然这个文件应当是存在和可读的。

在创建一个FileOutputStream对象时指定文件如果存在,将要被覆盖。

1
2
3
FileInputStream inOne = new FileInputStream("hello.test");
File f = new File();
FileInputStream inTwo = new FileInputStream(f);

创建一个FileOutputStream对象时,可以为其指定还不存在的文件名,但不能是存在的目录名,也不能是一个已被其他程序打开了的文件。

FileOutputStream先创建输出对象,然后再准备输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestFileStream {
public static void main(String[] args) throws IOException {
File f = new File("hello.txt");
FileOutputStream out = new FileOutputStream(f);
byte buf[]="hello ipcreator".getBytes();
out.write(buf);
out.close();

FileInputStream in = new FileInputStream(f);
byte [] contentBuf = new byte[1024];
int len = in.read(contentBuf);
System.out.println(new String(buf,0,len));
}
}

FileInputStream,FileOutputStream这两个类只提供了对字节或字节数组进行读取的方法,对于字符串的读写,我们还需要进行额外的转换。

FileOutputStream没有使用缓冲区。

Reader与Writer

Java为字符文本的输入输出专门提供了一套单独的类,Reader和Writer两个抽象类与InputStream,OutputStream两个类相对应,同样,Reader和Writer下面也有许多子类,对具体IO设备进行字符输入输出,如FileReader就是用来读取文件流中的字符。

FileWriter使用了缓冲区。

PipedInputStream与PipedOutputStream

这两个类主要用来完成线程之间的通信,一个线程的PipedInputStream对象能够从另外一个线程的PipedOutputStream对象中读取数据。

JDK还提供了PipedWriter和PipedReader这两个类来用于字符文本的管道通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class TestPipedReader {
public static void main(String[] args) throws IOException {

Writer writer = new Writer();
Reader reader = new Reader();
Thread writerThread = new Thread(writer);
Thread readerThread = new Thread(reader);

PipedWriter pipedWriter = writer.getPipedWriter();
PipedReader pipedReader = reader.getPipedReader();
pipedWriter.connect(pipedReader);

writerThread.start();
readerThread.start();
}
}

class Writer implements Runnable{

private PipedWriter writer = new PipedWriter();

public PipedWriter getPipedWriter(){
return writer;
}

@Override
public void run() {
try {
writer.write("hello Reader");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

class Reader implements Runnable{

private PipedReader reader = new PipedReader();

public PipedReader getPipedReader(){
return reader;
}

@Override
public void run() {

char[] buf = new char[1024];
int len;
try {
len = reader.read(buf);
reader.close();
System.out.println(new String(buf,0,len));
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用管道流类,可以实现各个程序模块之间的松耦合通信,我们可以灵活地将多个这样的模块的输出流与输入流相连接,以拼装成满足各种应用的程序,而不用对模块内部进行修改。假设有一个使用了管道流的压缩或加密的模块,我们的调用程序只管向该模块的输入流中送入数据,从该模块的输出流中取得数据,就完成了我们数据的压缩或加密,这个模块就像黑匣子一样,我们根本不用去了解它的任何细节。

ByteArrayInputStream 与ByteArrayOutputStream

1
2
3
4
5
ByteArrayInputStream(byte[] buf)
Creates a ByteArrayInputStream so that it uses buf as its buffer array.

ByteArrayInputStream(byte[] buf, int offset, int length)
Creates ByteArrayInputStream that uses buf as its buffer array.
1
2
3
4
5
6
7
// 创建一个32字节的缓冲区
ByteArrayOutputStream()
Creates a new byte array output stream.

// 根据参数指定的大小创建缓冲区,缓冲区的大小在数据过多时能够自动增长
ByteArrayOutputStream(int size)
Creates a new byte array output stream, with a buffer capacity of the specified size, in bytes.

这两个流的作用在于,用IO流的方式来完成对字节数组内容的读写,我们为什么不直接读写字节数组呢?在什么情况下该使用这两个类呢?

内存虚拟文件/内存映像文件,他们是把一块内存虚拟成一个硬盘上的文件,原来该写到硬盘文件上的内容会被写到这个内存中,原来该从一个硬盘文件上读取内容可以改为从内存中直接读取。如果程序在运行过程中药产生一些临时文件,就可以用虚拟文件的方式来实现,我们不用访问硬盘,而是直接访问内存,会提高应用程序的效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestArrayStream {

public static void main(String[] args) throws IOException {

String temp = "hello ipcreator";
byte[] src = temp.getBytes();
ByteArrayInputStream input = new ByteArrayInputStream(src);
ByteArrayOutputStream output = new ByteArrayOutputStream();
transform(input,output);
System.out.println(new String(output.toByteArray()));
}

public static void transform(InputStream in, OutputStream out)
throws IOException{
int c =0;
while((c=in.read())!=-1){
int C=(int)Character.toUpperCase(c);
out.write(C);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestArrayReader {

public static void main(String[] args) throws IOException {

StringReader reader = new StringReader("hello arrayreader");
StringWriter writer = new StringWriter();
convert(reader,writer);
System.out.println(new String(writer.toString()));
}

public static void convert(Reader reader, Writer writer)
throws IOException{

char [] charBuff= new char[1024];
int len = reader.read(charBuff);
writer.write(new String(charBuff,0,len).toUpperCase());
}
}

IO程序代码的复用

Reference

[1]everyone-can-use-english
[2]Java就业教程张孝祥PDF版.pdf
[3]Think In Java(中文第四版).pdf

欢迎关注我的其它发布渠道