Java线上应用故障排查之二:高内存占用

前一篇介绍了线上应用故障排查之一:高CPU占用,这篇主要分析高内存占用故障的排查。

Java开发的,经常会碰到下面两种异常:

1、java.lang.OutOfMemoryError: PermGen space

2、java.lang.OutOfMemoryError: Java heap space

要详细解释这两种异常,需要简单重提下Java内存模型。

(友情提示:本博文章欢迎转载,但请注明出处:hankchen,http://www.blogjava.net/hankchen

Java内存模型是描述Java程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节。

在Java虚拟机中,内存分为三个代:新生代(New)、老生代(Old)、永久代(Perm)。

(1)新生代New:新建的对象都存放这里

(2)老生代Old:存放从新生代New中迁移过来的生命周期较久的对象。新生代New和老生代Old共同组成了堆内存。

(3)永久代Perm:是非堆内存的组成部分。主要存放加载的Class类级对象如class本身,method,field等等。

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。

一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。

 

从代码的角度,软件开发人员主要关注java.lang.OutOfMemoryError: Java heap space异常,减少不必要的对象创建,同时避免内存泄漏。

现在以一个实际的例子分析内存占用的故障排查。

2G19({7(0}N(FIL09LH175N

通过top命令,发现PID为9004的Java进程一直占用比较高的内存不释放(24.7%),出现高内存占用的故障。

想起上一篇线上应用故障排查之一:高CPU占用介绍的PS命令,能否找到具体是哪个的线程呢?

ps -mp 9004 -o THREAD,tid,time,rss,size,%mem

1

遗憾的是,发现PS命令可以查到具体进程的CPU占用情况,但是不能查到一个进程下具体线程的内存占用情况。

 

只好寻求其他方法了,幸好Java提供了一个很好的内存监控工具:jmap命令

jmap命令有下面几种常用的用法:

•jmap [pid]

•jmap -histo:live [pid] >a.log

•jmap -dump:live,format=b,file=xxx.xxx [pid]

用得最多是后面两个。其中,jmap -histo:live [pid] 可以查看当前Java进程创建的活跃对象数目和占用内存大小。

jmap -dump:live,format=b,file=xxx.xxx [pid] 则可以将当前Java进程的内存占用情况导出来,方便用专门的内存分析工具(例如:MAT)来分析。

这个命令对于分析是否有内存泄漏很有帮助。具体怎么使用可以查看本博的另一篇文章:利用Eclipse Memory Analyzer Tool(MAT)分析内存泄漏

 

这里详细介绍下jmap -histo:live [pid] 命令:

1

从上图可以看出,int数组、constMethodKlass、methodKlass、constantPoolKlass都占用了大量的内存。

特别是占用了大量内存的int数组,需要仔细检查相关代码。

其中:

[C is a char[]
[S is a short[]
[I is a int[]
[B is a byte[]
[[I is a int[][]

[C对象占用Heap多,往往跟String有关,String其内部使用final char[]数组来保存数据的。

constMethodKlass/ methodKlass/ constantPoolKlass/ constantPoolCacheKlass/ instanceKlassKlass/ methodDataKlass

与Classloader相关,常驻与Perm区。

最后,总结下排查内存故障的方法和技巧有哪些:

1、top命令:Linux命令。可以查看实时的内存使用情况。

2、jmap -histo:live [pid],然后分析具体的对象数目和占用内存大小,从而定位代码。

3、jmap -dump:live,format=b,file=xxx.xxx [pid],然后利用MAT工具分析是否存在内存泄漏等等。

(友情提示:本博文章欢迎转载,但请注明出处:hankchen,http://www.blogjava.net/hankchen

线上应用故障排查之一:高CPU占用

一个应用占用CPU很高,除了确实是计算密集型应用之外,通常原因都是出现了死循环。

(友情提示:本博文章欢迎转载,但请注明出处:hankchen,http://www.blogjava.net/hankchen

以我们最近出现的一个实际故障为例,介绍怎么定位和解决这类问题。

clip_image002

根据top命令,发现PID为28555的Java进程占用CPU高达200%,出现故障。

通过ps aux | grep PID命令,可以进一步确定是tomcat进程出现了问题。但是,怎么定位到具体线程或者代码呢?

首先显示线程列表:

ps -mp pid -o THREAD,tid,time

1

找到了耗时最高的线程28802,占用CPU时间快两个小时了!

其次将需要的线程ID转换为16进制格式:

printf “%x\n” tid

2

最后打印线程的堆栈信息:

jstack pid |grep tid -A 30

3

找到出现问题的代码了!

现在来分析下具体的代码:ShortSocketIO.readBytes(ShortSocketIO.java:106)

ShortSocketIO是应用封装的一个用短连接Socket通信的工具类。readBytes函数的代码如下:

public byte[] readBytes(int length) throws IOException {

if ((this.socket == null) || (!this.socket.isConnected())) {

throw new IOException(“++++ attempting to read from closed socket”);

}

byte[] result = null;

ByteArrayOutputStream bos = new ByteArrayOutputStream();

if (this.recIndex >= length) {

bos.write(this.recBuf, 0, length);

byte[] newBuf = new byte[this.recBufSize];

if (this.recIndex > length) {

System.arraycopy(this.recBuf, length, newBuf, 0, this.recIndex – length);

}

this.recBuf = newBuf;

this.recIndex -= length;

} else {

int totalread = length;

if (this.recIndex > 0) {

totalread -= this.recIndex;

bos.write(this.recBuf, 0, this.recIndex);

this.recBuf = new byte[this.recBufSize];

this.recIndex = 0;

}

int readCount = 0;

while (totalread > 0) {

if ((readCount = this.in.read(this.recBuf)) > 0) {

if (totalread > readCount) {

bos.write(this.recBuf, 0, readCount);

this.recBuf = new byte[this.recBufSize];

this.recIndex = 0;

} else {

bos.write(this.recBuf, 0, totalread);

byte[] newBuf = new byte[this.recBufSize];

System.arraycopy(this.recBuf, totalread, newBuf, 0, readCount – totalread);

this.recBuf = newBuf;

this.recIndex = (readCount – totalread);

}

totalread -= readCount;

}

}

}

问题就出在标红的代码部分。如果this.in.read()返回的数据小于等于0时,循环就一直进行下去了。而这种情况在网络拥塞的时候是可能发生的。

至于具体怎么修改就看业务逻辑应该怎么对待这种特殊情况了。

 

最后,总结下排查CPU故障的方法和技巧有哪些:

1、top命令:Linux命令。可以查看实时的CPU使用情况。也可以查看最近一段时间的CPU使用情况。

2、PS命令:Linux命令。强大的进程状态监控命令。可以查看进程以及进程中线程的当前CPU使用情况。属于当前状态的采样数据。

3、jstack:Java提供的命令。可以查看某个进程的当前线程栈运行情况。根据这个命令的输出可以定位某个进程的所有线程的当前运行状态、运行代码,以及是否死锁等等。

4、pstack:Linux命令。可以查看某个进程的当前线程栈运行情况。

(友情提示:本博文章欢迎转载,但请注明出处:hankchen,http://www.blogjava.net/hankchen

mongostat

  • inserts/s 每秒插入次数
  • query/s 每秒查询次数

注:10条简单的查询可能比一条复杂的查询速度还快, 所以数值的大小,意义并不大。

  • update/s 每秒更新次数
  • delete/s 每秒删除次数
  • getmore/s查询时游标(cursor)的getmore操作,每秒执行getmore次数
  • command/s 每秒的命令数,比以上插入、查找、更新、删除的综合还多,还统计了别的命令,在主从系统中,会显示两个值 (例如:80|0),分别代表 本地|复制 命令的个数
  • flushs/s 每秒执行fsync将数据写入硬盘的次数。

注:一般都是0,或者1,通过计算两个1之间的间隔时间,可以大致了解多长时间flush一次。flush开销是很大的,如果频繁的flush,可能就要找找原因了。

  • mapped/s 所有的被mmap的数据量,单位是MB,
  • vsize 虚拟内存使用量,单位MB
  • res 物理内存使用量,单位MB

注:mapped, vsize一般不会有大的变动, res会慢慢的上升,如果res经常突然下降,去查查是否有别的程序狂吃内存。

  • faults/s 每秒访问失败数(只有Linux有),数据被交换出物理内存,放到swap。不要超过100,否则就是机器内存太小,造成频繁swap写入。此时要升级内存或者扩展,大压力下这个数值往往不为0。如果经常不为0,那就该加内存了。
  • locked % 被锁的时间百分比,尽量控制在50%以下吧
  • idx miss % 索引不命中所占百分比。如果太高的话就要考虑索引是不是少了
  • qr  客户端等待从 MongoDB 实例读取数据的队列长度。
  • qw  客户端等待向 MongoDB 实例写入数据的队列长度。
  • ar  执行读取操作的活动客户端的数目。
  • aw  执行写入操作的活动客户端的数目。
  • q t|r|w 当Mongodb接收到太多的命令而数据库被锁住无法执行完成,它会将命令加入队列。这一栏显示了总共、读、写3个队列的长度,都为0的话表示mongo毫无压力。高并发时,一般队列值会升高。
  • netIn
  • netOut 网络带宽压力
  • conn 当前连接数

注: MongoDB为每一个连接创建一个线程,线程的创建和释放也是有开销的。尽量不要让这个数值很大

  • time 时间戳

Web通信

为达到实时通信,一类是基于HTTP的Comet推送技术,另一类是基于套接口(Socket)传送信息实现消息传输。

一、目前使用Comet主要有两种方式,轮询和iframe流。

  • 轮询 polling
    浏览器周期性的发出请求,如果服务器没有新数据需要发送就返回以空响应。这种方法问题很大:首先,大量无意义的请求造成网络压力;其次,请求周期的限制不能及时地获得最新数据。这种方法很快就被淘汰。
  • 长轮询 long polling
    长轮询是在打开一条连接以后保持连接,等待服务器推送来数据再关闭连接。然后浏览器再发出新的请求,这能更好地管理请求数量,也能及时地更新数据。AJAX调用XMLHttpRequest对象发出HTTP请求,JS响应处理函数根据服务器返回的数据更新HTML页面的展示。这个方法一定程度上消除了简单轮询的弊端,但服务器压力也是很大。
  • iframe流 iframe streaming
    iframe流方式是在页面中插入一个隐藏的iframe,利用其src属性在服务器和客户端之间建立一条长链接,服务器向iframe传输数据(通常是HTML,内有负责插入信息的javascript),来实时更新页面。”iframe是很早就存在的一种 HTML 标记,通过在 HTML 页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 SRC属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。”其不足为:进度条会显示一直,反应在页面上就是浏览器标签页的图标会不停地转动。(当然这也是有解决方法的)

    二、基于WebSocket

    HTML5提供的Websocket不同于上面这些在老的HTML已有框架内的方法,而是在单个TCP连接上进行全双工通讯的协议。目前主流浏览器都已支持。
    1. 初始化过程
    不同于早期JAVA使用在浏览器安装插件的方法——-Java Applet 套接口:这种方法不足在于Java Applet再收到服务器返回的消息后,无法通过Javascript去更新HTML页面的内容。而是通过HTTP建立连接(HTTP handshake)。
    2. 开始通讯
    一旦初始连接建立,浏览器和服务器就打开了一个TCP socket的频道。在这个频道内就能进行双向的数据通信。

    然而Websocket依然有一些问题。比如浏览器兼容性问题(随着浏览器的发展,肯定是越来越小的),以及网络中间物(代理服务、防火墙)问题不支持WebSocket,这时Socket.io的出现就是为了完善WebSocket。

  • Socket.IO

    Guillermo Rauch在2010年开发第一版时,目的很明确地指向Node.js实时应用。在几次版本更新后,重新定义和封装核心功能而分化出一个基础模块 Engine.io——力求建立更稳定的工具。Engine.IO有着更稳定的连接质量。使得Socket.IO在先打开一个长轮询,再在将连接推至WebSocket频道继续通信。
    在使用Node的http模块创建服务器同时还要Express应用,因为这个服务器对象需要同时充当Express服务和Socket.io服务。

java修饰符static和final

static表示不要实例化就可以使用,静态变量只在加载类的过程中分配一次内存,没有被static修饰的实例变量,在每次实例化时都分配一次内存。

static final用来修饰成员变量和成员方法,可简单理解为“全局常量”!
对于变量,表示一旦给值就不可修改,并且通过类名可以访问。
对于方法,表示不可覆盖,并且可以通过类名直接访问。

修饰符
名称 说明 备注
static 静态变量(又称为类变量,其它的称为实例变量) 可以被类的所有实例共享。并不需要创建类的实例就可以访问静态变量
final 常量,值只能够分配一次,不能更改 注意不要使用const,虽然它和C、C++中的const关键字含义一样可以同static一起使用,避免对类的每个实例维护一个拷贝
transient 告诉编译器,在类对象序列化的时候,此变量不需要持久保存 主要是因为改变量可以通过其它变量来得到,使用它是为了性能的问题
volatile 指出可能有多个线程修改此变量,要求编译器优化以保证对此变量的修改能够被正确的处理

final类不能被继承,没有子类,final类中的方法默认是final的。
final方法不能被子类的方法覆盖,但可以被继承。
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
final不能用于修饰构造方法。
注意:父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的。

static变量前可以有private修饰,表示这个变量可以在类的静态代码块中,或者类的其他静态成员方法中使用(当然也可以在非静态成员方法中使用–废话),但是不能在其他类中通过类名来直接引用,这一点很重要。private是访问权限限定,static表示不要实例化就可以使用。

 

1、static变量

        静态变量
        按照是否静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另一种是没有被static修饰的变量,叫实例变量。两者的区别是:
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
对于实例变量,没创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。

2、静态方法
        静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联!这个需要去理解,想明白其中的道理,不是记忆!!!
因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
3、static代码块
static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。例如:
public class Test5 {
private static int a;
private int b;static {
Test5.a = 3;
System.out.println(a);
Test5 t = new Test5();
t.f();
t.b = 1000;
System.out.println(t.b);
}static {
Test5.a = 4;
System.out.println(a);
}public static void main(String[] args) {
// TODO 自动生成方法存根
}

static {
Test5.a = 5;
System.out.println(a);
}

public void f() {
System.out.println(“hhahhahah”);
}
}

运行结果:
3
hhahhahah
1000
4
5
        利用静态代码块可以对一些static变量进行赋值,最后再看一眼这些例子,都一个static的main方法,这样JVM在运行main方法的时候可以直接调用而不用创建实例。

MongoDB使用笔记

MongoDB安装并设置成Windows服务的教程:http://www.weste.net/2014/10-31/99742.html
使用mongod –dbpath f:/MongoDB/data 命令开启服务后才能使用mongo命令
mongod.exe –dbpath f:/MongoDB/data

插入数据:db.hy.insert({id:2016,userName:’hy’,age:21});

查询数据:db.hy.find();

设置服务:mongod –logpath F:\MongoDB\logs\mongodb.log –logappend –dbpath F:\MongoDB\data –serviceName MongoDB –install
shell操作:
1.创建:db.a.insert({“name”:”jack”,”age”:19})
db.a.insert({“name”:”luce”,”age”:20})

2.查找:db.a.find() –全查
db.a.findOne() –只取多个的第一个
db.a.find({“name”:”jack”}) –根据条件查找
db.getCollection(‘user’).find({}).limit(1)
db.getCollection(‘user’).find({}).skip(20).limit(10)
db.getCollection(‘user’).find({“$or”:[{“uname”:/辣鸡/},{“telephone”:/1314147/}]})
db.getCollection(‘user’).find({“_id”:{$in:[“11915″,”17446”]}})
db.getCollection(‘user’).find({“_id”:{$nin:[“11915″,”17446”]}})

3.更新:db.a.update({“name”:”jack”},{“age”:30})

4.删除:db.a.remove() –删除所有文档,集合保留
db.a.remove({“name”:”jack”}) –删除复合条件的文档
db.a.drop() –删除集合,于此同时所有文档也删除了
5.或操作:db.getCollection(‘tournamentMatch’).find({“$or”:[{‘playerA.uid’:’12486′},{‘playerB.uid’:’12988′}]})

6.update:db.collection.update( criteria, objNew, upsert, multi )

criteria : update的查询条件,类似sql update查询内where后面的
objNew : update的对象和一些更新的操作符(如$,$inc…)等,也可以理解为sql update查询内set后面的
upsert : 这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
multi : mongodb默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。

db.getCollection(“club”).update({“_id”:clubId},{$set:{“chairmanId”:newChairmanId}},false,false)

7.复制线上数据库db.copyDatabase(“laosiji”, “laosiji-server”, “123.56.187.38”, “laosiji”, “laosijI2016”, “SCRAM-SHA-1”)

8.操作符
(>) 大于 – $gt
(<) 小于 – $lt
(>=) 大于等于 – $gte
(<= ) 小于等于 – $lte、
db.col.find({“id” : {$gt : 100}})

9.多条件与、或、非操作
http://blog.csdn.net/subuser/article/details/46415739
http://www.blogjava.net/xiaomage234/archive/2012/08/06/384904.html

10.db.getCollection(‘club’).find({“name”:/哥哥.*/}) :包含哥哥这两个字
db.getCollection(‘club’).find({“name”:/^好哥哥/}) :以好哥哥开头

11.删除数据库某个字段
db.user.update({},{$unset:{“num”:0}},false,true)//可以对多条数据更新 true字段

12.重命名自段
db.user.update({}, {$rename : {“name” : “uname”}}, false, true)

13.类型转换
mongo可以通过find(…).forEach(function(x) {})语法来修改collection的field类型。
假设collection为foo,field为bad:
转换为int类型:
db.foo.find({bad: {$exists: true}}).forEach(function(obj) {
obj.user_id = new NumberInt(obj.user_id);
db.foo.save(obj);
});
同理转换为string类型:
db.foo.find( { bad : { $exist : true } } ).forEach( function (x) {
x.bad = new String(x.bad); // convert field to string
db.foo.save(x);
});
14.加索引命令
db.user.createIndex({“uname”:1})
15.判断某个字段是否存在
db.getCollection(‘signUpRecord’).find({“tourStartTime”:{$exists:true}})
16.排序
db.getCollection(‘user’).find({}).sort({“websitePoint”:-1}).limit(10)

17.脚本的// 查询写法
var matchCursor = db.match.find({“_id”:{$regex:challengeId}});

18.pull
db.getCollection(‘friends’).update({“appliedList.uid”:”17269″},{$pull:{“appliedList”:{“uid”:”17269″}}},false,true)

19.数组查询
db.getCollection(‘userHonor’).find({“honorList.0”:{$exists:1}})
db.getCollection(‘userHonor’).find({“honorList”:{$size:1}})
db.getCollection(‘userHonor’).find({$where:”this.honorList.length>0″})
db.getCollection(‘userHonor’).find({“honorList”:{$elemMatch:{“honor”:”goddess”,”level”:1}}})
$where 在走投无路的时候可以用,但它的效率是很低的。

JAVA笔记

Java笔记

Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒之后失效。如果为负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。如果为0,表示删除该Cookie。默认为–1

一、JAVA集合主要分为三种类型:

  • Set(集)
  • List(列表)
  • Map(映射)

Collection 接口 :Collection是最基本的集合接口,声明了适用于JAVA集合(只包括Set和List)的通用方法。 Set 和List 都继承了Conllection,Map

1.Set(集合): Set是最简单的一种集合。集合中的对象不按特定的方式排序,并且没有重复对象。 Set接口主要实现了两个实现类:

  • HashSet: HashSet类按照哈希算法来存取集合中的对象,存取速度比较快
  • TreeSet :TreeSet类实现了SortedSet接口,能够对集合中的对象进行排序。

Set 的用法:存放的是对象的引用,没有重复对象

Set set=new HashSet();

String s1=new String("hello");

String s2=s1;

String s3=new String("world");

set.add(s1);

set.add(s2);

set.add(s3);

System.out.println(set.size());//打印集合中对象的数目 为 2。

  • HashSet:为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。
  • TreeSet: 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
  • LinkedHashSet:具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。

 

2.List(列表): List的特征是其元素以线性方式存储,集合中可以存放重复对象。

List接口主要实现类包括:

  • ArrayList() : 代表长度可以改变得数组。可以对元素进行随机的访问,向ArrayList()中插入与删除元素的速度慢。
  • LinkedList(): 在实现中采用链表数据结构。插入和删除速度快,访问速度慢。

List:次序是List最重要的特点:它保证维护元素特定的顺序。List为Collection添加了许多方法,使得能够向List中间插入与移除元素(这只推 荐LinkedList使用。)一个List可以生成ListIterator,使用它可以从两个方向遍历List,也可以从List中间插入和移除元 素。

ArrayList:由数组实现的List。允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。ListIterator只应该用来由后向前遍历 ArrayList,而不是用来插入和移除元素。因为那比LinkedList开销要大很多。

LinkedList :对顺序访问进行了优化,向List中间插入与删除的开销并不大。随机访问则相对较慢。(使用ArrayList代替。)还具有下列方 法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 这些方法 (没有在任何接口或基类中定义过)使得LinkedList可以当作堆栈、队列和双向队列使用。

3.Map(映射): 

Map 是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承于Collection接口 从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

总结:Set对每个对象只接受一次,并使用自己内部的排序方法(通常,你只关心某个元素是否属于 Set,而不关心它的顺序–否则应该使用List)。Map同样对每个元素保存一份,但这是基于”键”的,Map也有内置的排序,因而不关心元素添加的 顺序。如果添加元素的顺序对你很重要,应该使用 LinkedHashSet或者LinkedHashMap.

  • Map : 维护“键值对”的关联性,使你可以通过“键”查找“值”
  • HashMap:Map基于散列表的实现。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量capacity和负载因子load factor,以调整容器的性能。
  • LinkedHashMap: 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序。
  • TreeMap : 基于红黑树数据结构的实现。查看“键”或“键值对”时,它们会被排序(次序由Comparabel或Comparator决定)。TreeMap的特点在 于,你得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。
  • WeakHashMao :弱键(weak key)Map,Map中使用的对象也被允许释放: 这是为解决特殊问题设计的。如果没有map之外的引用指向某个“键”,则此“键”可以被垃圾收集器回收。
  • IdentifyHashMap: : 使用==代替equals()对“键”作比较的hash map。专为解决特殊问题而设计。

二、Java中共有8种基本数据类型,包括4 种整型、2 种浮点型、1 种字符型、1 种布尔型,请见下表。

Java基本数据类型
数据类型 说明 所占内存 举例 备注
byte 字节型 1 byte 3, 127
short 短整型 2 bytes 3, 32767
int 整型 4 bytes 3, 21474836
long 长整型 8 bytes 3L, 92233720368L long最后要有一个L字母(大小写无所谓)。
float 单精度浮点型 4 bytes 1.2F, 223.56F float最后要有一个F字母(大小写无所谓)。
double 双精度浮点型 8 bytes 1.2, 1.2D, 223.56, 223.56D double最后最好有一个D字母(大小写无所谓)。
char 字符型 2 bytes ‘a’, ‘A’ 字符型数据只能是一个字符,由单引号包围。
boolean 布尔型 1 bit true, false

 

float 类型有效数字最长为 7 位,有效数字长度包括了整数部分和小数部分。例如:

  1. floatx = 223.56F;
  2. floaty = 100.00f;

注意:每个float类型后面都有一个标志“F”或“f”,有这个标志就代表是float类型。

double 类型有效数字最长为 15 位。与 float 类型一样,double 后面也带有标志“D”或“d”。例如:

  1. doublex = 23.45D;
  2. doubley = 422.22d;
  3. doublez = 234;

注意:不带任何标志的浮点型数据,系统默认是 double 类型。

三、二进制、八进制、十六进制:

八进制有一个前缀 0,例如 010 对应十进制中的 8;十六进制有一个前缀 0x,例如 0xCAFE;从 Java 7 开始,可以使用前缀 0b 来表示二进制数据,例如 0b1001 对应十进制中的 9。同样从 Java 7 开始,可以使用下划线来分隔数字,类似英文数字写法,例如 1_000_000 表示 1,000,000,也就是一百万。下划线只是为了让代码更加易读,编译器会删除这些下划线。

四、运算符及类型转换

自动转换按从低到高的顺序转换。不同类型数据间的优先关系如下:
低———————————————>高
byte,short,char-> int -> long -> float -> double
运算中,不同类型的数据先转化为同一类型,然后进行运算,转换规则如下:

操作数1类型 操作数2类型 转换后的类型
byte、short、char int int
byte、short、char、int long long
byte、short、char、int、long float float
byte、short、char、int、long、float double double

移位:

value << num
num 指定要移位值value 移动的位数。
左移的规则只记住一点:丢弃最高位,0补最低位
如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模。如对int型移动33位,实际上只移动了33%32=1位。

五、StringBuilder类

StringBuilder类和StringBuffer类功能基本相似,方法也差不多,主要区别在于StringBuffer类的方法是多线程安全的,而StringBuilder不是线程安全的,相比而言,StringBuilder类会略微快一点。

StringBuffer、StringBuilder、String中都实现了CharSequence接口。

CharSequence是一个定义字符串操作的接口,它只包括length()、charAt(int index)、subSequence(int start, int end) 这几个API。

StringBuffer、StringBuilder、String对CharSequence接口的实现过程不一样,如下图所示:
图1  对CharSequence接口的实现
可见,String直接实现了CharSequence接口;StringBuilder 和 StringBuffer都是可变的字符序列,它们都继承于AbstractStringBuilder,实现了CharSequence接口。

总结

线程安全:

  • StringBuffer:线程安全
  • StringBuilder:线程不安全

速度:
一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。

使用环境:

  • 操作少量的数据使用 String;
  • 单线程操作大量数据使用 StringBuilder;
  • 多线程操作大量数据使用 StringBuffer。
  • 类及实例化

局部变量:在方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。

成员变量:成员变量是定义在类中、方法体之外的变量。这种变量在创建对象的时候实例化(分配内存)。成员变量可以被类中的方法和特定类的语句访问。

类变量:类变量也声明在类中,方法体之外,但必须声明为static类型。

  • 构造方法不能被显示调用。
  • 构造方法不能有返回值,因为没有变量来接收返回值。

 

在Java中,使用new关键字来创建对象,一般有以下三个步骤:

  • 声明:声明一个对象,包括对象名称和对象类型。
  • 实例化:使用关键字new来创建一个对象。
  • 初始化:使用new创建对象时,会调用构造方法初始化对象。
  • 访问修饰符

Java中所谓的“friendly”和“default”都只是一种说法,并不是说真有那么一个指定默认访问权限的关键字。Java中类的成员权限修饰符只有三个:public/private/protected。

修饰符 说明
public 共有的,对所有类可见。
protected 受保护的,对同一包内的类和所有子类可见。
private 私有的,在同一类内可见。
默认的 在同一包内可见。默认不使用任何修饰符。

 

protected:受保护的

被声明为protected的变量、方法和构造方法能被同一个包中的任何其他类访问,也能够被不同包中的子类访问。

protected访问修饰符不能修饰类和接口,方法和成员变量能够声明为protected,但是接口的成员变量和成员方法不能声明为protected。

子类能访问protected修饰符声明的方法和变量,这样就能保护不相关的类使用这些方法和变量。

默认的:不使用任何关键字

不使用任何修饰符声明的属性和方法,对同一个包内的类是可见的。接口里的变量都隐式声明为public static final,而接口里的方法默认情况下访问权限为public。

访问控制和继承

请注意以下方法继承(不了解继承概念的读者可以跳过这里,或者点击 Java继承和多态 预览)的规则:

父类中声明为public的方法在子类中也必须为public。

父类中声明为protected的方法在子类中要么声明为protected,要么声明为public。不能声明为private。

父类中默认修饰符声明的方法,能够在子类中声明为private。

父类中声明为private的方法,不能够被继承。

访问修饰符
名称 说明 备注
public 可以被任何类访问
protected 可以被同一包中的所有类访问

可以被所有子类访问

子类没有在同一包中也可以访问
private 只能够被 当前类的方法访问
缺省

无访问修饰符

可以被同一包中的所有类访问 如果子类没有在同一个包中,也不能访问

 

修饰符
名称 说明 备注
static 静态变量(又称为类变量,其它的称为实例变量) 可以被类的所有实例共享。

并不需要创建类的实例就可以访问静态变量

final 常量,值只能够分配一次,不能更改 注意不要使用const,虽然它和C、C++中的const关键字含义一样

可以同static一起使用,避免对类的每个实例维护一个拷贝

transient 告诉编译器,在类对象序列化的时候,此变量不需要持久保存 主要是因为改变量可以通过其它变量来得到,使用它是为了性能的问题
volatile 指出可能有多个线程修改此变量,要求编译器优化以保证对此变量的修改能够被正确的处理

 

  • 变量的作用域和this关键字

 

在Java中,变量的作用域分为四个级别:类级、对象实例级、方法级、块级。

类级变量又称全局级变量或静态变量,需要使用static关键字修饰,你可以与 C/C++ 中的 static 变量对比学习。类级变量在类定义后就已经存在,占用内存空间,可以通过类名来访问,不需要实例化。

对象实例级变量就是成员变量,实例化后才会分配内存空间,才能访问。

方法级变量就是在方法内部定义的变量,就是局部变量。

块级变量就是定义在一个块内部的变量,变量的生存周期就是这个块,出了这个块就消失了,比如 if、for 语句的块。块是指由大括号包围的代码

public class Demo{
    {
        int j = 2;// 块级变量  属性块,在类初始化属性时候运行
    }
}

this 关键字用来表示当前对象本身,this 只有在类实例化后才有意义。super 关键字与 this 类似,this 用来表示当前类的实例,super 用来表示父类。super 不是一个对象的引用,不能将 super 赋值给另一个对象变量,它只是一个指示编译器调用父类方法的特殊关键字。

匿名对象就是没有名字的对象

new B(this).print(); // 匿名对象 (没有B b = new B(),所以是匿名对象)。

  • 方法重载

同一个类中的多个方法可以有相同的名字,只要它们的参数列表不同就可以,这被称为方法重载

  • 声明为final的方法不能被重载。
  • 声明为static的方法不能被重载,但是能够被再次声明。
  • 仅仅返回类型不同不足以成为方法的重载。
  • Java类的运行顺序
  1. publicclass Demo{                                      
  2.     private String name;
  3.     private int age;
  4.     public Demo(){
  5.         name = "namestr";
  6.         age = 22;
  7.     }
  8.      public static void main(String[] args){
  9.         Demo obj = new Demo();
  10.         System.out.println(obj.name + "的年龄是" + obj.age);
  11.     }
  12. }

基本运行顺序是:

  1. 先运行到第 9 行,这是程序的入口。
  2. 然后运行到第 10 行,这里要 new 一个Demo,就要调用 Demo 的构造方法。
  3. 就运行到第 5 行,注意:可能很多人觉得接下来就应该运行第 6 行了,错!初始化一个类,必须先初始化它的属性。
  4. 因此运行到第 2 行,然后是第 3 行。
  5. 属性初始化完过后,才回到构造方法,执行里面的代码,也就是第 6 行、第 7 行。
  6. 然后是第8行,表示 new 一个Demo实例完成。
  7. 然后回到 main 方法中执行第 11 行。
  8. 然后是第 12 行,main方法执行完毕。
  9.  父类静态块
  10.  自身静态块
  11.  父类块
  12.  父类构造器
  13.  自身块
  14.  自身构造器

Java执行步骤:

父类静态(静态块和静态成员变量谁在前先执行谁)>子类静态(静态块和静态成员变量)>父类块和成员变量>父类构造器>子类块和成员变量>子类构造器>

  • Java包装类、拆箱和装箱

Java为每种基本数据类型分别设计了对应的类,称之为包装类(Wrapper Classes)。

基本数据类型及对应的包装类
基本数据类型 对应的包装类
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean
  • 由基本类型向对应的包装类转换称为装箱,例如把 int 包装成 Integer 类的对象;
  • 包装类向对应的基本类型转换称为拆箱,例如把 Integer 类的对象重新简化为 int。

1)  int 和 Integer 的相互转换

int m = 500;

Integer obj = new Integer(m); // 手动装箱

int n = obj.intValue(); // 手动拆箱

Java 1.5 之后可以自动拆箱装箱:

int m = 500;

Integer obj = m; // 自动装箱

int n = obj; // 自动拆箱

2) 将字符串转换为整数

int m = Integer.parseInt(“123”, 10);

只有是“123”这样的字符串才可以转换为整数,否则会抛出异常。

3) 将整数转换为字符串

int m = 500;

String s = Integer.toString(m);

  • Java多态和动态绑定

方法重写(覆盖):函数名相同,参数列表相同,返回值类型相同

方法重载:函数名相同,必须具有不同的参数列表(参考不同的构造方法)

父类的变量可以引用父类的实例,也可以引用子类的实例。比如Animal的变量可以引用动物的实例,也可以引用猫的实例,因为猫也是动物,但反过来不行。

多态:指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,但是如果子类重写了父类中的方法,那么调用的时候就会使用子类的这些方法。继承是子类获得父类的成员,重写是继承后重新实现父类的方法。重载是在一个类里一系列参数不同名字相同的方法。多态则是用基类的引用指向子类的对象。

动态绑定

为了理解多态的本质,下面讲一下Java调用方法的详细流程。
1) 编译器查看对象的声明类型和方法名。
假设调用 obj.func(param),obj 为 Cat 类的对象。需要注意的是,有可能存在多个名字为func但参数签名不一样的方法。例如,可能存在方法 func(int) 和 func(String)。编译器将会一一列举所有 Cat 类中名为func的方法和其父类 Animal 中访问属性为 public 且名为func的方法。

这样,编译器就获得了所有可能被调用的候选方法列表。

2) 接下来,编泽器将检查调用方法时提供的参数签名。
如果在所有名为func的方法中存在一个与提供的参数签名完全匹配的方法,那么就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,如果调用 func(“hello”),编译器会选择 func(String),而不是 func(int)。由于自动类型转换的存在,例如 int 可以转换为 double,如果没有找到与调用方法参数签名相同的方法,就进行类型转换后再继续查找,如果最终没有匹配的类型或者有多个方法与之匹配,那么编译错误。
这样,编译器就获得了需要调用的方法名字和参数签名。

3) 如果方法的修饰符是private、static、final(static和final将在后续讲解),或者是构造方法,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式 称为静态绑定(static binding)。
与此对应的是,调用的方法依赖于对象的实际类型, 并在运行时实现动态绑。例如调用 func(“hello”),编泽器将采用动态绑定的方式生成一条调用 func(String) 的指令。

4)当程序运行,并且釆用动态绑定调用方法时,JVM一定会调用与 obj 所引用对象的实际类型最合适的那个类的方法。我们已经假设 obj 的实际类型是 Cat,它是 Animal 的子类,如果 Cat 中定义了 func(String),就调用它,否则将在 Animal 类及其父类中寻找。

每次调用方法都要进行搜索,时间开销相当大,因此,JVM预先为每个类创建了一个方法表(method lable),其中列出了所有方法的名称、参数签名和所属的类。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在上面的例子中,JVM 搜索 Cat 类的方法表,以便寻找与调用 func(“hello”) 相匹配的方法。这个方法既有可能是 Cat.func(String),也有可能是 Animal.func(String)。注意,如果调用super.func(“hello”),编译器将对父类的方法表迸行搜索。

假设 Animal 类包含cry()、getName()、getAge() 三个方法,那么它的方法表如下:
cry() -> Animal.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()

实际上,Animal 也有默认的父类 Object(后续会讲解),会继承 Object 的方法,所以上面列举的方法并不完整。

假设 Cat 类覆盖了 Animal 类中的 cry() 方法,并且新增了一个方法 climbTree(),那么它的参数列表为:
cry() -> Cat.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()
climbTree() -> Cat.climbTree()

在运行的时候,调用 obj.cry() 方法的过程如下:

  • JVM 首先访问 obj 的实际类型的方法表,可能是 Animal 类的方法表,也可能是 Cat 类及其子类的方法表。
  • JVM 在方法表中搜索与 cry() 匹配的方法,找到后,就知道它属于哪个类了。
  • JVM 调用该方法。

方法调用的优先问题 ,优先级由高到低依次为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。(先调用当前的方法,没有则调用父类的方法,还没有则调用当前方法中的父类参数,再没有则调用父类方法中的父类参数)

  • 多态对象的类型转换

instanceof 运算符用来判断一个变量所引用的对象的实际类型,注意是它引用的对象的类型,不是变量的类型。如果变量a引用的是当前类ClassXX或它的子类的实例,a instanceof ClassXX 返回 true,否则返回 false。

在继承链中,我们将子类向父类转换称为“向上转型”,将父类向子类转换称为“向下转型”。
很多时候,我们会将变量定义为父类的类型,却引用子类的对象,这个过程就是向上转型。

不能直接将父类的对象强制转换为子类类型,只能将向上转型后的子类对象再次转换为子类类型。也就是说,子类对象必须向上转型后,才能再向下转型。

SuperClass superObj = new SuperClass();

SonClass sonObj = new SonClass();

 

// 下面的代码运行时会抛出异常,不能将父类对象直接转换为子类类型

// SonClass sonObj2 = (SonClass)superObj;

 

// 先向上转型,再向下转型

superObj = sonObj;(此时superObj instanceof SonClass为true,因为superObj 是 SonClass 类的实例)

SonClass sonObj1 = (SonClass)superObj;

  • static关键字和final关键字

静态变量也叫类变量,静态方法也叫类方法。

实例变量只能通过对象来访问,不能通过类访问,静态变量可以通过类直接访问。

  1. static关键字修饰的属性是类级别数据,它的数据是共享的,无需创建类的实例就可以访问,且只分配一个内存空间(一个线程修改了值,其他线程访问就变了)(推荐使用类来访问:Person.id)
    非static关键字修饰的元素是对象级别的,是各自拥有一份而之间没有任何关系的。

 

关于静态变量和静态方法的总结:

  • 一个类的静态方法只能访问静态变量;
  • 一个类的静态方法不能够直接调用非静态方法;
  • 如访问控制权限允许,静态变量和静态方法也可以通过对象来访问,但是不被推荐;
  • 静态方法中不存在当前对象,因而不能使用this,当然也不能使用 super;
  • 静态方法不能被非静态方法覆盖;
  • 构造方法不允许声明为 static 的;
  • 局部变量不能使用static修饰。

静态初始器(静态块)

块是由大括号包围的一段代码。静态初始器(Static Initializer)是一个存在于类中、方法外面的静态块。静态初始器仅仅在类装载的时候(第一次使用类的时候)执行一次,往往用来初始化静态变量。

静态导入

静态导入是 Java 5 的新增特性,用来导入类的静态变量和静态方法。

import static java.lang.System.*;

    import static java.lang.Math.random;

    public class Demo {

        public static void main(String[] args) {

        out.println("产生的一个随机数:" + random());

    }

}

因为System.out是静态方法。

final关键字:

  • final 修饰的类不能被继承。
  • final 修饰的方法不能被子类重写。
  • final 修饰的变量(成员变量或局部变量)即成为常量,只能赋值一次。
  • final 修饰的成员变量必须在声明的同时赋值,如果在声明的时候没有赋值,那么只有 一次赋值的机会,而且只能在构造方法中显式赋值,然后才能使用。
  • final 修饰的局部变量可以只声明不赋值,然后再进行一次性的赋值。

final 也可以用来修饰类(放在 class 关键字前面),阻止该类再派生出子类。方法也可以被 final 修饰,被 final 修饰的方法不能被覆盖;变量也可以被 final 修饰,被 final 修饰的变量在创建对象以后就不允许改变它们的值了。一旦将一个类声明为 final,那么该类包含的方法也将被隐式地声明为 final,但是变量不是。被 final 修饰的方法为静态绑定,不会产生多态(动态绑定),被 static 或 private 修饰的方法会被隐式的声明为 final,因为动态绑定没有意义。

Java Object类:

在Java中,只有基本类型不是对象,例如数值、字符和布尔型的值都不是对象,所有的数组类型,不管是对象数组还是基本类型数组都是继承自 Object 类。

  • equals()方法只能比较引用类型,“==”可以比较引用类型及基本类型。
  • 如果两个对象相同,那么它们的 hashCode 值一定要相同;如果两个对象的 hashCode 值相同,它们并不一定相同。
  • Java内部类及其实例化

在 Java 中,允许在一个类(或方法、语句块)的内部定义另一个类,称为内部类(Inner Class),有时也称为嵌套类(Nested Class)。

使用内部类的主要原因有:

  • 内部类可以访问外部类中的数据,包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来。
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
  • 减少类的命名冲突。

public class Outer {

    private int size;

    public class Inner {

        private int counter = 10;

        public void doStuff() {

            size++;

        }

    }

    public static void main(String args[]) {

        Outer outer = new Outer();

        Inner inner = outer.new Inner();//A.B b = new A().new B();

        inner.doStuff();

        System.out.println(outer.size);

        System.out.println(inner.counter);

    }

}

内部类可以是静态(static)的,可以使用 public、protected 和 private 访问控制符,而外部类只能使用 public,或者默认。

成员式内部类

在外部类内部直接定义(不在方法内部或代码块内部)的类就是成员式内部类,它可以直接使用外部类的所有变量和方法,即使是 private 的。外部类要想访问内部类的成员变量和方法,则需要通过内部类的对象来获取。

请看下面的代码:

public class Outer{

    private int size;

    public class Inner {

        public void dostuff() {

            size++;

       }

    }

    public void testTheInner() {

        Inner in = new Inner();

        in.dostuff();

    }

}

成员式内部类如同外部类的一个普通成员。

成员式内部类可以使用各种修饰符,包括 public、protected、private、static、final 和 abstract,也可以不写。

若有 static 修饰符,就为类级,否则为对象级。类级可以通过外部类直接访问,对象级需要先生成外部的对象后才能访问。

非静态内部类中不能声明任何 static 成员。

内部类可以相互调用,例如:

classA {

    // B、C 间可以互相调用

    class B {}

    class C {}

}

成员式内部类的访问

内部类的对象以成员变量的方式记录其所依赖的外层类对象的引用,因而可以找到该外层类对象并访问其成员。该成员变量是系统自动为非 static 的内部类添加的,名称约定为“outClassName.this”。

1) 使用内部类中定义的非静态变量和方法时,要先创建外部类的对象,再由“outObjectName.new”操作符创建内部类的对象,再调用内部类的方法,如下所示:

public class Demo{

    public static void main(String[] args) {

        Outer outer = new Outer();

        Outer.Inner inner = outer.new Inner();

        inner.dostuff();

    }

}

class Outer{

    private int size;

    class Inner{

        public void dostuff() {

        size++;

    }

    }

}

2) static 内部类相当于其外部类的 static 成员,它的对象与外部类对象间不存在依赖关系,因此可直接创建。示例如下:

publicclass Demo{

public static void main(String[] args) {

Outer.Inner inner = new Outer.Inner();

inner.dostuff();

}

}

  1. classOuter{
  2. private static int size;
  3. static class Inner {
  4. public void dostuff() {
  5. size++;
  6. System.out.println(“size=” + size);
  7. }
  8. }
  9. }

运行结果:
size=1

3) 由于内部类可以直接访问其外部类的成分,因此当内部类与其外部类中存在同名属性或方法时,也将导致命名冲突。所以在多层调用时要指明,如下所示:

  1. publicclass Outer{
  2. private int size;
  3. public class Inner{
  4. private int size;
  5. public void dostuff(int size){
  6. size++;  // 局部变量 size;
  7. this.size;  // 内部类的 size
  8. Outer.this.size++;  // 外部类的 size
  9. }
  10. }
  11. }

局部内部类

局部内部类(Local class)是定义在代码块中的类。它们只在定义它们的代码块中是可见的。

局部类有几个重要特性:

  1. 仅在定义了它们的代码块中是可见的;
  2. 可以使用定义它们的代码块中的任何局部final 变量;
  3. 局部类不可以是 static 的,里边也不能定义 static 成员;
  4. 局部类不可以用 public、private、protected 修饰,只能使用缺省的;
  5. 局部类可以是 abstract 的。

请看下面的代码:

  1. publicclass Outer {
  2. public static final int TOTAL_NUMBER = 5;
  3. public int id = 123;
  4. public void func() {
  5. final int age = 15;
  6. String str = “http://www.weixueyuan.net”;
  7. class Inner {
  8. public void innerTest() {
  9. System.out.println(TOTAL_NUMBER);
  10. System.out.println(id);
  11. // System.out.println(str);不合法,只能访问本地方法的final变量
  12. System.out.println(age);
  13. }
  14. }
  15. new Inner().innerTest();
  16. }
  17. public static void main(String[] args) {
  18. Outer outer = new Outer();
  19. outer.func();
  20. }
  21. }

运行结果:
5
123
15

匿名内部类

匿名内部类是局部内部类的一种特殊形式,也就是没有变量名指向这个类的实例,而且具体的类实现会写在这个内部类里面。

注意:匿名类必须继承一个父类或实现一个接口。

不使用匿名内部类来实现抽象方法:

  1. abstractclass Person {
  2. public abstract void eat();
  3. }
  4. classChild extends Person {
  5. public void eat() {
  6. System.out.println(“eat something”);
  7. }
  8. }
  9. publicclass Demo {
  10. public static void main(String[] args) {
  11. Person p = new Child();
  12. p.eat();
  13. }
  14. }

运行结果:
eat something

可以看到,我们用Child继承了Person类,然后实现了Child的一个实例,将其向上转型为Person类的引用。但是,如果此处的Child类只使用一次,那么将其编写为独立的一个类岂不是很麻烦?

这个时候就引入了匿名内部类。使用匿名内部类实现:

  1. abstractclass Person {
  2. public abstract void eat();
  3. }
  4. publicclass Demo {
  5. public static void main(String[] args){
  6. // 继承 Person 类
  7. new Person() {
  8. public void eat() {
  9. System.out.println(“eat something”);
  10. }
  11. }.eat();
  12. }
  13. }

可以看到,匿名类继承了 Person 类并在大括号中实现了抽象类的方法。

内部类的语法比较复杂,实际开发中也较少用到,本教程不打算进行深入讲解,各位读者也不应该将内部类作为学习Java的重点。

  • 抽象类

如果一个类没有足够的信息来描述一个具体的对象,而需要其他具体的类来支撑它,那么这样的类我们称它为抽象类。比如new Animal(),我们都知道这个是产生一个动物Animal对象,但是这个Animal具体长成什么样子我们并不知道,它没有一个具体动物的概念,所以他就是一个抽象类,需要一个具体的动物,如狗、猫来对它进行特定的描述,我们才知道它长成啥样。

在自上而下的继承层次结构中,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看,祖先类更加通用,它只包含一些最基本的成员,人们只将它作为派生其他类的基类,而不会用来创建对象。

在面向对象领域由于抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能实例化的。

只给出方法定义而不具体实现的方法被称为抽象方法,抽象方法是没有方法体的,在代码的表达上就是没有“{}”。包含一个或多个抽象方法的类也必须被声明为抽象类。
使用 abstract 修饰符来表示抽象方法和抽象类。

抽象类不能被实例化,抽象方法必须在子类中被实现。

不能有抽象构造方法或抽象静态方法。

无法从静态上下文引用静态变量。

public class TestPro {

    private String str = "1231542";

    abstract class Animal {

        //抽象类中可以有自己的方法,比如setter、getter方法

        public abstract void cry();

    }

    class Cat extends Animal {

       @Override

        public void cry() {

            System.out.println("猫叫:喵喵...");

        }

    }

    class Dog extends Animal {

        @Override

        public void cry() {

            System.out.println("狗叫:汪汪...");

        }

    }

    public static void main(String[] args) {

        TestPro.Animal a1 = new TestPro().new Cat();

        TestPro testPro = new TestPro();

        TestPro.Dog a2 = testPro.new Dog();

        System.out.println(testPro.str);

        a1.cry();

        a2.cry();

    }

}

  • 接口

在抽象类中,可以包含一个或多个抽象方法;但在接口(interface)中,所有的方法必须都是抽象的,不能有方法体,它比抽象类更加“抽象”。(Java 8的default方法,可以在接口内部包含一些默认的方法实现)

接口中声明的成员变量默认都是 public static final 的,必须显示的初始化。因而在常量声明时可以省略这些修饰符。

1) 接口中只能定义抽象方法,这些方法默认为 public abstract 的,因而在声明方法时可以省略这些修饰符。试图在接口中定义实例变量、非抽象的实例方法及静态方法,都是非法的。例如:

public interface SataHdd{

    //连接线的数量

    public int connectLine; //编译出错,connectLine被看做静态常量,必须显式初始化

    //写数据

    protected void writeData(String data); //编译出错,必须是public类型

    //读数据

    public static String readData(){ //编译出错,接口中不能包含静态方法

        return "数据"; //编译出错,接口中只能包含抽象方法,

    }

}

  • 接口中没有构造方法,不能被实例化。
    3) 一个接口不实现另一个接口,但可以继承多个其他接口。接口的多继承特点弥补了类的单继承。

实现接口的格式如下:
修饰符 class 类名 extends 父类 implements 多个接口 {
实现方法
}

  • 抽象类与接口
  • 都不能被实例化。
  • 抽象类可以为部分方法提供实现,避免了在子类中重复实现这些方法,提高了代码的可重用性,这是抽象类的优势;而接口中只能包含抽象方法,不能包含任何实现。
  • 一个类只能继承一个直接的父类(可能是抽象类),但一个类可以实现多个接口,这个就是接口的优势。

抽象类方式中,抽象类可以拥有任意范围的成员数据,同时也可以拥有自己的非抽象方法,但是接口方式中,它仅能够有静态、不能修改的成员数据(但是我们一般是不会在接口中使用成员数据),同时它所有的方法都必须是抽象的。在某种程度上来说,接口是抽象类的特殊化。

对子类而言,它只能继承一个抽象类(这是java为了数据安全而考虑的),但是却可以实现多个接口。

abstract class Door{

    abstract void open();

    abstract void close();

}

interface Alarm{

    void alarm();

}

class AlarmDoor extends Door implements Alarm{

    void open(){}

    void close(){}

    void alarm(){}

}

综上所述,接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:

  • 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
  • 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。
  • 泛型

 

// 定义泛型类

class Point<T1, T2>{ //注意类型参数位置

    T1 x;

    T2 y;

    public T1 getX() {

        return x;

    }

    public void setX(T1 x) {

        this.x = x;

    }

    public T2 getY() {

        return y;

    }

    public void setY(T2 y) {

        this.y = y;

    }

    // 定义泛型方法

    public <T1, T2> void printPoint(T1 x, T2 y){ //类型参数需要放在修饰符后面、返回值类型前面

        T1 m = x;

        T2 n = y;

        System.out.println("This point is:" + m + ", " + n);

    }

}

T1, T2 是自定义的标识符,也是参数,用来传递数据的类型,而不是数据的值,我们称之为类型参数。习惯上使用单个大写字母,通常情况下,K 表示键,V 表示值,E 表示异常或错误,T 表示一般意义上的数据类型。

 

public <T extends Number> T getMax(T array[]){

    T max = null;

    for(T element : array){

        max = element.doubleValue() > max.doubleValue() ? element : max;

    }

    return max;

}

  • Java 泛型使用 <? super T> <? extends T>
    <? extends T> T类的某一种子类, 表示包括T在内的任何T的子类
    <? super T> T类的某一种超类, 表示包括T在内的任何T的父类
    请记住PECS原则:生产者(Producer)使用extends,消费者(Consumer)使用super。
  • <T extends Number> 表示 T 只接受 Number 及其子类,传入其他类型的数据会报错。这里的限定使用关键字 extends,后面可以是类也可以是接口。如果是类,只能有一个;但是接口可以有多个,并以“&”分隔,例如 <T extends Interface1 & Interface2>。

这里的 extends 关键字已不再是继承的含义了,应该理解为 T 是继承自 Number 类的类型,或者 T 是实现了 XX 接口的类型。

  • 异常处理

Java异常处理通过5个关键字控制:try、catch、throw、throws和 finally。

try {
    // block of code to monitor for errors
}
catch (ExceptionType1 exOb) {
    // exception handler for ExceptionType1
}
finally {
    // block of code to be executed before try block ends
}

Finally块中的代码在任何方法返回之前都一定会被执行。

class Test {

    static void exc(){

        try{

            throw new NullPointerException();

        }

        catch(NullPointerException e){

            System.out.println("空指针:"+e);

           throw e;

        }

    }

    public static void main(String[] args) {

        try{

           exc();

       }catch(NullPointerException ex){

            System.out.println("再次捕获异常:"+ex);

        }

    }

}

throw是语句抛出一个异常。
语法:throw (异常对象);
throw e;

throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常)
语法:[(修饰符)](返回值类型)(方法名)([参数列表])[throws(异常类)]{……}
public void doA(int a) throws Exception1,Exception3{......}

如:

void doA(int a) throws Exception1,Exception3{
    try{
        ......

    }catch(Exception1 e){
        throw e;
    }catch(Exception2 e){
       System.out.println("出错了!");
    }
    if(a!=b)
        throw new  Exception3("自定义异常");
}

为exception2已经被处理了(System.out.println),所以该方法可能会抛出exception1和exception3异常。

throw语句用在方法体内,表示抛出异常,由方法体内的语句处理。
throws语句用在方法声明后面,表示再抛出异常,由该方法的调用者来处理。

throws主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常。
throw是具体向外抛异常的动作,所以它是抛出一个异常实例。

  • Finally块

finally创建一个代码块。该代码块在一个try/catch 块完成之后另一个try/catch出现之前执行。finally块无论有没有异常抛出都会执行。

try {
    System.out.println("inside procA");
    throw new RuntimeException("demo");
} finally {
    System.out.println("procA's finally");
}

  • java 异常处理 Throwable Error 和Exception

Checked exception需要开发者自己去进行异常处理,不然编译无法通过。而unchecked exception开发者可以不进行异常处理程序也可以正常编译,但程序运行到异常的地方会自动抛出异常(上面的RuntimeException都是unchecked的)。

  • unchecked exception(非检查异常)

也称运行时异常(RuntimeException),比如常见的NullPointerException、IndexOutOfBoundsException。对于运行时异常,java编译器不要求必须进行异常捕获处理或者抛出声明,由程序员自行决定。

  • checked exception(检查异常,编译异常)

也称非运行时异常(运行时异常以外的异常就是非运行时异常),java编译器强制程序员必须进行捕获处理,比如常见的IOExeption和SQLException。对于非运行时异常如果不进行捕获或者抛出声明处理,编译都不会通过。

  • Error(错误)

是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。

  • Exception(异常)

是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。

  • 区别
  1. Checked异常必须被显式地捕获或者传递,而unchecked异常则可以不必捕获或抛出。
  2. Checked异常继承lang.Exception类。Unchecked异常继承自java.lang.RuntimeException类。

除了Error与RuntimeException,其他剩下的异常都是你需要关心的,而这些异常类统称为Checked Exception,至于Error与RuntimeException则被统称为Unchecked Exception。

当程序执行过程中,遇到uncheck exception,则程序中止,不再执行之后的代码。

表 1-1 Java 的 java.lang 中定义的未检查异常子类
异常 说明
ArithmeticException 算术错误,如被0除
ArrayIndexOutOfBoundsException 数组下标出界
ArrayStoreException 数组元素赋值类型不兼容
ClassCastException 非法强制转换类型
IllegalArgumentException 调用方法的参数非法
IllegalMonitorStateException 非法监控操作,如等待一个未锁定线程
IllegalStateException 环境或应用状态不正确
IllegalThreadStateException 请求操作与当前线程状态不兼容
IndexOutOfBoundsException 某些类型索引越界
NullPointerException 非法使用空引用
NumberFormatException 字符串到数字格式非法转换
SecurityException 试图违反安全性
StringIndexOutOfBounds 试图在字符串边界之外索引
UnsupportedOperationException 遇到不支持的操作

 

表 1-2  java.lang 中定义的检查异常
异常 意义
ClassNotFoundException 找不到类
CloneNotSupportedException 试图克隆一个不能实现Cloneable接口的对象
IllegalAccessException 对一个类的访问被拒绝
InstantiationException 试图创建一个抽象类或者抽象接口的对象
InterruptedException 一个线程被另一个线程中断
NoSuchFieldException 请求的字段不存在
NoSuchMethodException 请求的方法不存在

 

断言用于证明和测试程序的假设,比如“这里的值大于 5”。
断言可以在运行时从代码中完全删除,所以对代码的运行速度没有影响。

断言有两种方法:

  • 一种是 assert<<布尔表达式>> ;
  • 另一种是 assert<<布尔表达式>> :<<细节描述>>;

如果布尔表达式的值为false , 将抛出AssertionError 异常,并在异常中输出细节描述。

  • Java多线程

Java在进程间同步性的老模式基础上实行了另一种方法:管程(monitor)。一旦线程进入管程,所有线程必须等待直到该线程退出了管程。

public static void main(String[] args) {

    Thread t = Thread.currentThread();

    System.out.println(t);

    t.setName("wocao");

    System.out.println(t);

    try{

        for(int i=5;i>0;i--){

            System.out.println(i);

            Thread.sleep(1000);

        }

    }catch(InterruptedException e){

        System.out.println("Exception get");

    }

    System.out.println("finished");

}

输出结果:

Thread[main,5,main]
Thread[wocao,5,main]
5
4
3
2
1

该显示顺序:线程名称,优先级以及组的名称。默认情况下,主线程的名称是main。它的优先级是5,这也是默认值,main也是所属线程组的名称。一个线程组(thread group)是一种将线程作为一个整体集合的状态控制的数据结构。

  • Thread和Runnable
  • 在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口。继承Thread类的要重写run()方法,然后用它的实例执行start()方法。实现Runnable接口的要实现run()方法,然后用Thread的public Thread(Runnabletarget) 方法开启多线程。

但是一个类只能继承一个父类,这是继承Thread方法的局限。

  • Runnable接口和Thread之间的联系:

public class Thread extends Object implements Runnable

判定线程是否结束方法:第一种isAlive()。这种方法由Thread定义,如果所调用线程仍在运行,isAlive()方法返回true,如果不是则返回false。但isAlive()很少用到,等待线程结束的更常用的方法是调用join(),描述如下:
final void join( ) throws InterruptedException。

实现Runnable接口的一个例子:

import java.io.*;

import java.lang.*;

class Test implements Runnable

{

    public void run(){

        for(int i=0;i<5;i++){

            System.out.println(Thread.currentThread().getName()+":"+i);

            try{

            Thread.sleep(1000);

            }catch(InterruptedException e){

            System.out.println("Exception");

            }

        }

    }

    public static void main (String[] args) throws java.lang.Exception

    {

        Thread t1 = new Thread(new Test(),"线程1");

        Thread t2 = new Thread(new Test(),"线程2");

        t1.start();

        t2.start();

        try{

            t1.join();

            t2.join();

        } catch (InterruptedException ex) {

            System.out.println("thread Exception caught");

        }

        System.out.println("finished");

    }

}

打印结果:

线程1:0

线程2:0

线程1:1

线程2:1

线程1:2

线程2:2

线程1:3

线程2:3

线程1:4

线程2:4

finished

  • 线程同步

Synchronzied关键字的作用一个词概括就是:线程同步。它可以用来修改对象中的方法,将对象加锁。相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A,没有的话,直接运行。

Synchronzied关键字包括两种用法:synchronized 方法和 synchronized 块。

  1. synchronized 方法

如:public synchronized void accessVal(int newVal);

synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。

  1. synchronized 块

synchronized 块可以对方法的某一部分加锁,用起来更加方便。

如:

synchronized(syncObject) {

    //允许访问控制的代码

}

synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。

synchronzied(this)(获得的是一个该实例的锁(this是当前对象))。当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

public synchronized void Push(char c){同步代码;}
相当于:
public void Push(char c){
    synchronized(this){同步代码;}
}

  • 线程间通信

为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:

  • wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。
  • notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
  • notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。

这些方法在Object中被声明,如下所示:
final void wait( ) throws InterruptedException
final void notify( )
final void notifyAll( )
wait( )存在的另外的形式允许你定义等待时间。

 

public class Goods {

    private int seq = 0;

    private boolean hasOne = false;

    public synchronized int getSeq() {

        if (!hasOne) {

            try {

                this.wait();

            } catch (InterruptedException ex) {

                System.out.println("InterruptedException caught");

            }

        }

        System.out.println("得到了序号:" + seq);

        hasOne = false;

        notify();

        return seq;

    }

 

    public synchronized void setSeq(int seq) {

        if (hasOne) {

            try {

                wait();

            } catch (InterruptedException ex) {

                System.out.println("InterruptedException caught");

            }

        }

        System.out.println("设置了序号:" + seq);

        this.seq = seq;

        hasOne = true;

        notify();

    }

 

    public static void main(String[] args) {

        Goods goods = new Goods();

        Producer producer = new Producer(goods);

        Consumer consumer = new Consumer(goods);

    }

}

 

class Producer implements Runnable {

 

    Goods goods;

    public Producer(Goods goods) {

        this.goods = goods;

        new Thread(this, "producer").start();

    }

    public void run() {

        for (int i = 0; i < 5; i++) {

            goods.setSeq(i);

        }

    }

}

 

class Consumer implements Runnable {

    Goods goods;

    public Consumer(Goods goods) {

        this.goods = goods;

        new Thread(this, "consumer").start();

    }

 

    public void run() {

        for (int i = 0; i < 5; i++) {

            goods.getSeq();

        }

    }

}

运行结果:

设置了序号:0

得到了序号:0

设置了序号:1

得到了序号:1

设置了序号:2

得到了序号:2

设置了序号:3

得到了序号:3

设置了序号:4

得到了序号:4

  • 线程死锁

假定一个线程进入了对象X的管程而另一个线程进入了对象Y的管程。如果X的线程试图调用Y的同步方法,它将像预料的一样被锁定。而此时如果Y的线程再试图调用X的同步方法,则会造成死锁(互相等待对方释放锁)。

Class A{

    synchronized void caller(B b){

        b.last();

    }

    synchronized void last(B b){

    }

}

Class B{

    synchronized void caller(A a){

        a.last();

    }

    synchronized void last(A a){

    }

}

如上,当两个线程分别进入到A.m1()和B.m1()执行时,两个线程都请求对方的同步方法,此时会发生死锁。

  • 线程挂起、恢复和终止

先于Java2的版本,程序用Thread 定义的suspend() 和 resume() 来暂停和再启动线程。它们的形式如下:
final void suspend( ) //挂起线程
final void resume( ) //线程恢复,用此方法可以恢复上面挂起的线程

Thread定义的suspend(),resume()和stop()方法可能会造成严重的系统故障。假定对关键的数据结构的一个线程被锁定的情况,如果该线程在那里挂起,这些锁定的线程并没有放弃对资源的控制。其他的等待这些资源的线程可能死锁,所以在Java2被舍弃了。所以使用wait()和notify()方法控制线程的执行。

输入输出(IO)操作

  • 输入输出流

在Java中,把不同类型的输入输出源抽象为流,其中输入和输出的数据称为数据流(Data Stream)。

为了提高数据的传输效率,引入了缓冲流(Buffered Stream)的概念,即为一个流配备一个缓冲区(Buffer),一个缓冲区就是专门用于传送数据的一块内存。

I/O流类概述

为了方便流的处理,Java语言提供了java.io包,在该包中的每一个类都代表了一种特定的输入或输出流。为了使用这些流类,编程时需要引入这个包。 Java提供了两种类型的输入输出流:一种是面向字节的流,数据的处理以字节为基本单位;另一种是面向字符的流,用于字符数据的处理。字节流(Byte Stream)每次读写8位二进制数,也称为二进制字节流或位流。字符流一次读写16位二进制数,并将其做一个字符而不是二进制位来处理。需要注意的是,为满足字符的国际化表示,Java语言的字符编码采用的是16位的Unicode码,而普通文本文件中采用的是8位ASCⅡ码。

java.io中类的层次结构如图所示。
针对一些频繁的设备交互,Java语言系统预定了3个可以直接使用的流对象,分别是:

  • in(标准输入),通常代表键盘输入。
  • out(标准输出):通常写往显示器。
  • err(标准错误输出):通常写往显示器。

在Java语言中使用字节流和字符流的步骤基本相同,以输入流为例,首先创建一个与数据源相关的流对象,然后利用流对象的方法从流输入数据,最后执行close()方法关闭流。

java.IO层次体系结构:

在整个Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable.掌握了这些IO的核心操作那么对于Java中的IO体系也就有了一个初步的认识了

Java I/O主要包括如下几个层次,包含三个部分:

1.流式部分――IO的主体部分;

2.非流式部分――主要包含一些辅助流式部分的类,如:File类、RandomAccessFile类和FileDescriptor等类;

3.其他类–文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。

主要的类如下:

  1. File(文件特征与管理):用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。
  2. InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
  3. OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。

     Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节来表示。为此,JAVA中引入了处理字符的流。

  1. Reader(文件格式操作):抽象类,基于字符的输入操作。
  2. Writer(文件格式操作):抽象类,基于字符的输出操作。
  3. RandomAccessFile(随机文件操作):它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作

Java中IO流的体系结构如图:

 

 

IO框架:

 

    

IO流的具体分类

一、按I/O类型来总体分类:

  1.    Memory1)从/向内存数组读写数据: CharArrayReader、 CharArrayWriter、ByteArrayInputStream、ByteArrayOutputStream
    2)从/向内存字符串读写数据 StringReader、StringWriter、StringBufferInputStream
     2.Pipe管道  实现管道的输入和输出(进程间通信): PipedReader、PipedWriter、PipedInputStream、PipedOutputStream
    3.File 文件流。对文件进行读、写操作 :FileReader、FileWriter、FileInputStream、FileOutputStream
    4. ObjectSerialization 对象输入、输出 :ObjectInputStream、ObjectOutputStream
    5.DataConversion数据流 按基本数据类型读、写(处理的数据是Java的基本类型(如布尔型,字节,整数和浮点数)):DataInputStream、DataOutputStream
     6.Printing 包含方便的打印方法 :PrintWriter、PrintStream
    7.Buffering缓冲  在读入或写出时,对数据进行缓存,以减少I/O的次数:BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream
    8.Filtering 滤流,在数据进行读或写时进行过滤:FilterReader、FilterWriter、FilterInputStream、FilterOutputStream过
    9.Concatenation合并输入 把多个输入流连接成一个输入流 :SequenceInputStream
    10.Counting计数  在读入数据时对行记数 :LineNumberReader、LineNumberInputStream
    11.Peeking Ahead 通过缓存机制,进行预读 :PushbackReader、PushbackInputStream
    12.Converting between Bytes and Characters 按照一定的编码/解码标准将字节流转换为字符流,或进行反向转换(Stream到Reader,Writer的转换类):InputStreamReader、OutputStreamWriter

二、按数据来源(去向)分类:
1、File(文件): FileInputStream, FileOutputStream, FileReader, FileWriter
2、byte[]:ByteArrayInputStream, ByteArrayOutputStream
3、Char[]: CharArrayReader, CharArrayWriter
4、String: StringBufferInputStream, StringReader, StringWriter
5、网络数据流:InputStream, OutputStream, Reader, Writer

如何选择IO流:

1)确定是数据源和数据目的(输入还是输出)

源:输入流 InputStream Reader
目的:输出流 OutputStream Writer

2)明确操作的数据对象是否是纯文本

是:字符流Reader,Writer
否:字节流InputStream,OutputStream

3)明确具体的设备。

是硬盘文件:File++:

读取:FileInputStream,, FileReader,

写入:FileOutputStream,FileWriter
是内存用数组

byte[]:ByteArrayInputStream, ByteArrayOutputStream
是char[]:CharArrayReader, CharArrayWriter
是String:StringBufferInputStream(已过时,因为其只能用于String的每个字符都是8位的字符串), StringReader, StringWriter
是网络用Socket流

是键盘:用System.in(是一个InputStream对象)读取,用System.out(是一个OutoutStream对象)打印

3)是否需要转换流

是,就使用转换流,从Stream转化为Reader,Writer:InputStreamReader,OutputStreamWriter

4)是否需要缓冲提高效率

是就加上Buffered:BufferedInputStream, BufferedOuputStream, BuffereaReader, BufferedWriter
5)是否需要格式化输出

 

例:将一个文本文件中数据存储到另一个文件中。
1)数据源和数据目的:读取流,InputStream Reader  输出:OutputStream Writer
2)是否纯文本:是!这时就可以选择Reader Writer。
3)设备:是硬盘文件。Reader体系中可以操作文件的对象是 FileReader FileWriter。

FileReader fr = new FileReader(“a.txt”);

FileWriter fw = new FileWriter(“b.txt”);
4)是否需要提高效率:是,加Buffer
BufferedReader bfr = new BufferedReader(new FileReader(“a.txt”);  );
BufferedWriter bfw = new BufferedWriter(new FileWriter(“b.txt”);  );

 

面向字符的输入流:

Reader和Writer是java.io包中所有字符流的父类。由于它们都是抽象类,所以应使用它们的子类来创建实体对象,利用对象来处理相关的读写操作。Reader和Writer的子类又可以分为两大类:一类用来从数据源读入数据或往目的地写出数据(称为节点流),另一类对数据执行某种处理(称为处理流)。

面向字符的输入流类都是Reader的子类,其类层次结构如图所示。
Reader的类层次结构图
Reader 的主要子类及说明:

Reader 的主要子类
类名 功能描述
CharArrayReader 从字符数组读取的输入流
BufferedReader 缓冲输入字符流
PipedReader 输入管道
InputStreamReader 将字节转换到字符的输入流
FilterReader 过滤输入流
StringReader 从字符串读取的输入流
LineNumberReader 为输入数据附加行号
PushbackReader 返回一个字符并把此字节放回输入流
FileReader 从文件读取的输入流

Reader 所提供的方法如下表所示,可以利用这些方法来获得流内的位数据。

Reader 的常用方法
方法 功能描述
void close() 关闭输入流
void mark() 标记输入流的当前位置
boolean markSupported() 测试输入流是否支持 mark
int read() 从输入流中读取一个字符
int read(char[] ch) 从输入流中读取字符数组
int read(char[] ch, int off, int len) 从输入流中读 len 长的字符到 ch 内
boolean ready() 测试流是否可以读取
void reset() 重定位输入流
long skip(long n) 跳过流内的 n 个字符

 

  • 常用类库、向量与哈希

Math类

Math类提供了常用的数学运算方法以及Math.PI和Math.E两个数学常量。该类是final的,不能被继承,类中的方法和属性全部是静态,不允许在类的外部创建Math类的对象。因此,只能使用Math类的方法而不能对其作任何更改。下表列出了Math类的主要方法。

Math类的主要方法
方法 功能
int abs(int i) 求整数的绝对值(另有针对long、float、double的方法)
double ceil(double d) 不小于d的最小整数(返回值为double型)
double floor(double d) 不大于d的最大整数(返回值为double型)
int max(int i1,int i2) 求两个整数中最大数(另有针对long、float、double的方法)
int min(int i1,int i2) 求两个整数中最小数(另有针对long、float、double的方法)
double random() 产生0~1之间的随机数
int round(float f) 求最靠近f的整数
long round(double d) 求最靠近d的长整数
double sqrt(double a) 求平方根
double sin(double d) 求d的sin值(另有求其他三角函数的方法如cos,tan,atan)
double log(double x) 求自然对数
double exp(double x) 求e的x次幂(ex
double pow(double a, double b) 求a的b次幂

 

字符串类

字符串是字符的序列。在 Java 中,字符串无论是常量还是变量都是用类的对象来实现的。java.lang 提供了两种字符串类:String 类和 StringBuffer 类。

1.String 类
按照 Java 语言的规定,String 类是 immutable 的 Unicode 字符序列,其作用是实现一种不能改变的静态字符串。实际上,所有改变字符串的结果都是生成新的字符串,而不是改变原来字符串。
2.StringBuffer 类
String 类不能改变字符串对象中的内容,只能通过建立一个新串来实现字符串的变化。如果字符串需要动态改变,就需要用 StringBuffer 类。StringBuffer 类主要用来实现字符串内容的添加、修改、删除,也就是说该类对象实体的内存空间可以自动改变大小,以便于存放一个可变的字符序列。

StringBuffer 类提供的三种构造方法
构造方法 说明
StringBuffer() 使用该无参数的构造方法创建的 StringBuffer 对象,初始容量为 16 个字符,当对象存放的字符序列大于 16 个字符时,对象的容量自动增加。该对象可以通过 length()方法获取实体中存放的字符序列的长度,通过 capacity()方法获取当前对象的实际容量。
StringBuffer(int length) 使用该构造方法创建的 StringBuffer 对象,其初始容量为参数 length 指定的字符个数,当对象存放的字符序列的长度大于 length 时,对象的容量自动增加,以便存放所增加的字符。
StringBuffer(Strin str) 使用该构造方法创建的 StringBuffer 对象,其初始容量为参数字符串 str 的长度再加上 16 个字符。

 

几种 StringBuffer 类常用的方法
方法 说明
append() 使用 append() 方法可以将其他 Java 类型数据转化为字符串后再追加到 StringBuffer 的对象中。
insert(int index, String str) insert() 方法将一个字符串插入对象的字符序列中的某个位置。
setCharAt(int n, char ch) 将当前 StringBuffer 对象中的字符序列 n 处的字符用参数 ch 指定的字符替换,n 的值必须是非负的,并且小于当前对象中字符串序列的长度。
reverse() 使用 reverse()方法可以将对象中的字符序列翻转。
delete(int n, int m) 从当前 StringBuffer 对象中的字符序列删除一个子字符序列。这里的 n 指定了需要删除的第一个字符的下标,m 指定了需要删除的最后一个字符的下一个字符的下标,因此删除的子字符串从 n~m-1。
replace(int n, int m, String str) 用 str 替换对象中的字符序列,被替换的子字符序列由下标 n 和 m 指定。

 

 

  • 哈希表

数组和向量都可以存储对象,但对象的存储位置是随机的,也就是说对象本身与其存储位置之间没有必然的联系。当要查找一个对象时,只能以某种顺序(如顺序查找或二分查找)与各个元素进行比较,当数组或向量中的元素数量很多时,查找的效率会明显的降低。

一种有效的存储方式,是不与其他元素进行比较,一次存取便能得到所需要的记录。这就需要在对象的存储位置和对象的关键属性(设为 k)之间建立一个特定的对应关系(设为 f),使每个对象与一个唯一的存储位置相对应。在查找时,只要根据待查对象的关键属性 k 计算f(k)的值即可。如果此对象在集合中,则必定在存储位置 f(k)上,因此不需要与集合中的其他元素进行比较。称这种对应关系 f 为哈希(hash)方法,按照这种思想建立的表为哈希表。

Java 使用哈希表类(Hashtable)来实现哈希表,以下是与哈希表相关的一些概念:

  • 容量(Capacity):Hashtable 的容量不是固定的,随对象的加入其容量也可以自动增长。
  • 关键字(Key):每个存储的对象都需要有一个关键字,key 可以是对象本身,也可以是对象的一部分(如某个属性)。要求在一个 Hashtable 中的所有关键字都是唯一的。
  • 哈希码(Hash Code):若要将对象存储到 Hashtable 上,就需要将其关键字 key 映射到一个整型数据,成为 key 的哈希码。
  • 项(Item):Hashtable 中的每一项都有两个域,分别是关键字域 key 和值域 value(存储的对象)。Key 和 value 都可以是任意的 Object 类型的对象,但不能为空。
  • 装填因子(Load Factor):装填因子表示为哈希表的装满程度,其值等于元素数比上哈希表的长度。

public class Test {

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

        Hashtable h = new Hashtable();

        h.put("1", 1);

        h.put(2, 2);

        Set set = h.entrySet();

        for(Iterator i = set.iterator();i.hasNext();){

            System.out.println(i.next());

        }

        System.out.println("--------------------------------1");

        Set s = h.keySet();

        for(Iterator i = s.iterator();i.hasNext();){

            System.out.println(i.next());

        }

 

        System.out.println("--------------------------------2");

        Collection c = h.values();

        Iterator iterator = c.iterator();

        while(iterator.hasNext()){

            System.out.println(iterator.next());

        }

        System.out.println("--------------------------------3");

        for(Enumeration e = h.keys();e.hasMoreElements();){

            System.out.println(e.nextElement());

        }

        System.out.println("--------------------------------4");

        for(Enumeration e = h.elements();e.hasMoreElements();){

            System.out.println(e.nextElement());

        }

    }

}

输出结果:

1=1

2=2

——————————–1

1

2

——————————–2

1

2

——————————–3

1

2

——————————–4

1

2

  • 几个重要的java数据库访问类和接口

编写访问数据库的Java程序还需要几个重要的类和接口。

DriverManager类

DriverManager类处理驱动程序的加载和建立新数据库连接。DriverManager是java.sql包中用于管理数据库驱动程序的类。通常,应用程序只使用类DriverManager的getConnection()静态方法,用来建立与数据库的连接,返回Connection对象:
static Connection getConnection(String url,String username,String password)
指定数据的URL用户名和密码创建数据库连接对象。url的语法格式是:
jdbc:<数据库的连接机制>:<ODBC数据库名>。

Connection类

Connection类是java.sql包中用于处理与特定数据库连接的类。Connection对象是用来表示数据库连接的对象,Java程序对数据库的操作都在这种对象上进行。Connection类的主要方法有:

  1. Statement createStatement():创建一个Statement对象。
  2. Statement createStatement(int resultSetType,int resultSetConcurrency):创建一个Statement对象,生成具有特定类型的结果集。
  3. void commit():提交对数据库的改动并释放当前持有的数据库的锁。
  4. void rollback():回滚当前事务中的所有改动并释放当前连接持有的数据库的锁。
  5. String getCatalog():获得连接对象的当前目录。
  6. boolean isClose():判断连接是否已关闭。
  7. boolean isReadOnly():判断连接是否为只读模式。
  8. void setReadOnly():设置连接为只读模式。
  9. void close():释放连接对象的数据库和JDBC资源。

Statement类

Statement类是java.sql包中用于在指定的连接中处理SQL语句的类。数据库编程的要点是在程序中嵌入SQL命令。程序需要声明和创建连接数据库的Connection对象,并让该对象连接数据库。调用类DriverManager的静态方法getConnection()获得Connection对象,实现程序与数据库的连。然后,用Statement类声明SQL语句对象,并调用Connection对象的createStatement()方法,创建SQL语句对象。例如,以下代码创建语句对象sql:
Statement sql = null;
try{
    sql = con.createStatement();
}catch(SQLException e){}

ResultSet类

有了SQL语句对象后,调用语句对象的方法executeQuery()执行SQL查询,并将查询结果存放在一个用ResultSet类声明的对象中,例如,以下代码读取学生成绩表存于rs 对象中:
ResultSet rs = sql.executeQuery(“SELECT * FROM ksInfo”);
ResultSet对象实际上是一个由查询结果数据的表,是一个管式数据集,由统一形式的数据行组成,一行对应一条查询记录。在ResultSet对象中隐含着一个游标,一次只能获得游标当前所指的数据行,用next方法可取下一个数据行。用数据行的字段(列)名称或位置索引(自1开始)调用形如getXXX()方法获得记录的字段植 。以下是ResultSet对象的部分方法:

  1. byte getByte(int columnIndex):返回指定字段的字节值。
  2. Date getDate(int columnIndex):返回指定字段的日期值。
  3. float getFloat(int columnIndex):返回指定字段的浮点值。
  4. int getInt(int columnIndex):返回指定字段的整数值。
  5. String getString(int columnIndex):返回指定字段的字符串值。
  6. double getDouble(String columnName):返回指定字段的双精度值。
  7. long getLong(String columnName):返回指定字段的long型整值。
  8. boolean next():返回是否还有下一字段。

以上方法中的columnIndex是位置索引,用于指定字段,columnName是字段名。

用户需要在查询结果集上浏览,或前后移动、或显示结果集的指定记录,这称为可滚动结果集。程序要获得一个可滚动结果集,只要在获得SQL的语句对象时,增加指定结果集的两个参数即可。例如,以下代码:
Statement stmt = con.createStatement(type,concurrency);
ResultSet rs = stmt.executeQuery(SQL语句)
语句对象stmt的SQL查询就能得到相应类型的结果集。

  • int 型参数type决定可滚动集的滚动方式:
    • TYPE_FORWORD_ONLY,结果集的游标只能向下滚动。
    • TYPE_SCROLL_INSENSITIVE,游标可上下移动,当数据库变化时,当前结果集不变。
    • TYPE_SCROLL_SENSITIVE,游标可上下移动,当数据库变化时,当前结果集同步改变。
  • int 型参数concurrency决定数据库是否与可滚动集同步更新:
    • CONCUR_READ_ONLY,不能用结果集更新数据库中的表。
    • CONCUR_UPDATETABLE,能用结果集更新数据库中的表。

例如,以下代码利用连接对象connect,创建Statement对象stmt,指定结果集可滚动,并以只读方式读数据库:
stmt = connect.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_READ_ONLY);
可滚动集上另外一些常用的方法如下:

  1. boolean previous():将游标向上移动,当移到结果集的第一行时,返回false。
  2. void beforeFirst():将游标移结果集的第一行之前。
  3. void afterLast():将游标移到结果集的最后一行之后。
  4. void first():将游标移到第一行。
  5. void last():将游标移到最后一行。
  6. boolean isAfterLast():判游标是否在最后一行之后。
  7. boolean isBeforeFirst():判游标是否在第一行之前。
  8. boolean isLast():判游标是否在最后一行。
  9. boolean isFirst():判游标是否在第一行。
  10. int getRow():获取当前所指的行(行号自1开始编号,结果集空,返回0)。
  11. boolean absolute(int row):将游标移到row行。

 

  • 数据库插入、更新、删除记录

插入数据表记录有3种方案

一.使用Statement对象

实现插入数据表记录的SQL语句的语法是:
insert into 表名(字段名1,字段名2,……)value (字段值1,字段值2,……)
例如:
insert into ksInfo(考号,姓名,成绩,地址,简历)value(‘200701’,’张大卫’,534,’上海欧阳路218弄4-1202’,’xxx’)
实现同样功能的Java程序代码是:
sql = “insert intoksIno(考号,姓名,成绩,地址,简历)”;
sql= = sq1+ “value(‘”+txtNo.getTxt()+’,’”+txtName.getText(0”’,”;
sql = sql+txtScore.getText();
sql=sql+”,’”+txtAddr.getText()+”’,’”+txtResume.getText()+”’)”;
stmt.executeUpdate(sql);

二.使用ResultSet对象

使用ResultSet对象的方法moveToInsertRow()将数据表游标移到插入位置,输入数据后,用方法insertRow()插入记录。例如,以下示意代码:
String sql= “select * from ksInfo”;//生成SQL语句
ResultSet rs = stmt.executeQuery(sql);//获取数据表结果集
rs.moveToInsertRow();//将数据表游标移到插入记录位置
rs.updateString(1,’200701’);//向考号字段填入数据
rs.updateString(2,’张大卫’);//向名字字段填入数据
rs.updateInt(3,534);//向成绩字段填入数据
rs.updateString(4,’上海欧阳路218弄4-1202’);//向地址字段填入数据
rs.updateString(5,’’);//向简历字段填入数据
try{rs.insertRow();}catch(Exception e){};//完成插入

三.使用PrepareStatement对象

与使用Statement对象的方法类似,只是创建SQL语句时暂时用参数?表示值,然后由SQL语句对象生成PrepareStatement对象,插入时通过设定实际参数,实现记录的更新。示意代码如下:
sql = “insert into ksInfo(考号,姓名,成绩,地址,简历)value (?,?,?,?,’’)”;
PrepareStatement pStmt = connect.prepareStatement(sql);
pStmt.setString(1,’200701’);//向考号字段填入数据
pStmt. setString (2,’张大卫’);//向名字字段填入数据
pStmt.setInt(3,534);//向成绩字段填入数据
pStmt. setString (4,’上海欧阳路218弄4-1202’);//向地址字段填入数据
pStmt. setString (5,’’);//向简历字段填入数据
pStmt.executeUpdate();

修改数据表记录也有3种方案。

一.使用Statement对象

实现修改数据表记录的SQL语句的语法是:
update表名 set 字段名1 = 字段值1,字段名2 = 字段值2,……where特定条件
例如:
update ksInfo set 姓名 = ‘张小卫’where 姓名 = ‘张大卫’
先创建一个SQL语句,然砶调用Statement对象的executeUpdate()方法。例如,
sql = “update ksInfo set 姓名 = ‘”+txtName.getText();
sql = sql + “,成绩=”+txtScore.getText();
sql = sql +”,地址=’”+txtAddr.getText();
sql= sql+”’,,简历=’”+txtResume.getText()+”’where 考号=”+txtNo.getText();
stmt.executeUpdate(sql);

二.使用ResultSet对象

先建立ResultSet对象,然后直接设定记录的字段值,修改数据表的记录。例如,
String sql = “select * from ksInfo where 姓名=’张大卫’”;//生成SQL语句
ResultSet rs = stmt.executeQuery(sql);//获取数据表结果集
if(rs.next()){
rs.updateString(2,’张小卫’);
try{rs.updateRow();}catch(Exception e){}
}

三.使用PrepareStatement对象

创建SQL语句时,暂时用参数?表示值,然后由SQL语句对象生成PrepareStatement对象,接着通过设定实际参数实现记录的更新。示意代码:
sql = “update ksInfo set 姓名=? where 姓名 = ‘张大卫’;
PrepareStatement pStmt = connect.prepareStatement(sql);
pStmt.setString(2,’张小卫’);//向名字字段填入数据
pStmt.executeUpdate();

删除数据表也有3种方案

一.使用Statement对象

删除数据表记录的SQL语句的语法是:
delete from 表名 where 特定条件
例如 :
delete from ksInfo where 姓名 = ‘张大卫’
先创建一个SQL语句,然后调用Statement对象的executeUpdate()方法:
stmt.executeUpdate(sql);

二.使用ResultSet对象

先创建一个SQL语句,然后调用Statement对象的executeUpdate()方法。例如:
String sql = “select * from ksInfo where 姓名 = ‘张大卫’”;//生成SQL语句
ResultSet rs = stmt.executeQuery(sql);//获取数据表结果集
if(rs.next()){
rs.deleteRow();try{ rs.updateRow();}catch(Exception e){}
}

三.使用PrepareStatement对象

创建SQL语句时,暂时用参数?表示值,然后由SQL语句对象生成PrepareStatement对象,接着设定实际参数实现特定记录的删除。例如,以下示意代码:
sql = “delete form ksInfo where 姓名=?”;
PrepareStatement pStmt = connect.prepareStatement(sql);
pStmt.setString(2,’张大卫’);//给名字字段指定数据
pStmt.executeUpdate();