20145326 《Java程序设计》第6周学习总结
教材学习内容总结
第十章
一、使用InputStream与OutputStream
1.串流设计的概念
想活用输入/输出API,一定要先了解Java中如何以串流抽象化输入/输出概念,以及InputStream、OutputStream继承架构。如此一来,无论标准输入/输出、文档输入/输出、网络输入/输出、数据库输入/输出都可以用一致的操作来处理。Java将输入/输出抽象化为串流,数据有来源及目的地,衔接两者的是串流对象,其实数据就好比水,串流就好比水管,通过水管的衔接,水由一端流向另一端。这个比喻十分形象化!从应用程序角度来看,如果要将数据从来源取出,可以使用输入串流,如果要将数据写入目的地,可以使用输出串流。在Java中,输入串流代表对象为java.io.InputStream实例,输出串流代表对象为java.io.OutputStream实例。来源与目的地都不知道的情况下,怎么撰写程序?可以设计一个通用的dump()方法。
dump()方法接受InputStream与OutputStream的实例,分别代表读取数据的来源,以及输出数据的目的地。在进行InputStream与OutputStream的相关操作时若发生错误,会抛出java.io.IOException异常,在这里不特别处理,而是在dump()方法上声明throws,由调用dump()方法的客户端处理。在不使用InputStream与OutputStream时,必须使用close()方法关闭串流,由于InputStream与OutputStream操作了java.io.Closeable接口,其父接口为java.lang.AutoCloseable接口,因此可使用JDK7尝试自动关闭资源语法。每次从InputStream读入的数据,都会置入byte数组中,InputStream的read()方法每次会尝试读入byte数组长度的数据,并返回实际读入的字节,只要不是-1,就表示读取到数据,可以使用OutputStream的write()方法,指定要写出的byte数组、初始索引与数据长度。书上虽然没有正式介绍到网络程序设计,不过java.net.URL的使用很简单,只要指定网址,URL实例会自动进行HTTP协议,可以使用openStream方法取得InputStream实例,代表与网站连接的数据串流。
2.串流继承架构
还记得System.in与System.out吗?查看API文件的话,会发现它们分别是InputStream与PrintStream的实例,分别代表标准输入与标准输出,以个人计算机而言,通常对应至文本模式中的输入与输出。可以使用System的setIn()方法指定InputStream实例,重新指定标准输入来源。
还可以使用System的setOut()方法指定PrintStream实例,将结果输出至指定的目的地。
在这个范例中用PrintStream打包FileOutputStream,你操作PrintStream相关方法,PrintStream会代你操作FileOutputStream。除了System.in与System.out之外,还有个System.err为PrintStream实例,称为标准错误输入串流,它是用来立即显示错误信息的。
3.FileInputStream与FileOutputStream
FileInputStream是InputStream的子类,可以指定文件名创建实例,一旦创建文档就开启,接着就可以用来读取数据,FileOutputStream是OutputStream的子类,可以指定文件名创建实例,一旦创建文档就开启,接着就可以用来写出数据,无论FileInputStream还是FileOutputStream,不使用时都要用close()关闭文档。
4.ByteArrayInputStream与ByteArrayOutputStream
ByteArrayInputStream是InputStream的子类,可指定byte数组创建实例。一旦创建就可以将byte数组当作数据源进行读取。ByteArrayOutputStream是OutputStream的子类,可指定byte数组创建实例。一旦创建就可以将byte数组当作目的地写出数据。
5.串流处理装饰器
InputStream、OutputStream提供串流基本操作,如果想要为输入/输出的数据做加工处理,则可以使用打包器类。前面示范过的Scanner类就是作为打包器,其接受InputStream实例,你操作Scanner打包器相关方法,Scanner会实际操作打包的InputStream取得数据,并转换为你想要的数据类型。InputStream、OutputStream的一些子类也具有打包器的作用。前面介绍的PrintStream就是实际例子,你操作PrintStream的print()、println()等方法,PrintStream会自动转换为byte数组数据,利用打包的OutputStream进行输出。下面介绍几种常用的串流装饰器类。
第一种是BufferedInputStream与BufferedOutputStream,两者都具备缓冲作用。如果InputStream第一次read()时可以尽量读取足够的数据至内存的缓冲区,后续调用read()时先看看缓冲区是不是还有数据,如果有就从缓冲区读取,没有再从来源读取数据至缓冲区,这样减少从来源直接读取数据的次数,对读取效率将会有帮助。毕竟内存的访问速度较快。如果OutputStream每次write()时可将数据写入内存中的缓冲区,缓冲区满了再将缓冲区的数据写入目的地,这样可减少对目的地的写入次数,对写入效率也会有帮助。
第二种是DataInputStream与DataOutputStream,DataInputStream与DataOutputStream提供读取、写入Java基本数据类型的方法,像是读写int、double、boolean等的方法。这些方法会自动在指定的类型与字节间转换。
第三种是ObjectInputStream与ObjectOutputStream,ObjectInputStream与ObjectOutputStream可以将内存中的对象整个储存下来,之后再读入还原为对象。ObjectInputStream提供readObject()方法将数据读入为对象,而ObjectOutputStream提供writeObject()方法将对象写至目的地,可以被这两个方法处理的对象,必须操作java.io.Serializable接口,这个接口并没有定义任何方法,只是作为标示之用,表示这个对象是可以串行化的。如果在做对象串行化时,对象中某些数据成员不希望被写出,则可以标上transient关键字。
二、字符处理类
1.Reader与Writer继承架构
针对字符数据的读取,Java SE提供了java.io.Reader类,其抽象化了字符数据读入的来源,针对字符数据的写入,则提供了java.io.Writer类,其抽象化了数据写出的目的地。在不使用Reader与Writer时,必须使用close()方法关闭串流。Reader与Writer操作了Closeable接口,其父接口为AutoCloseable接口,因此可使用JDK7尝试自动关闭资源语法。StringReader可以将字符串打包,当作读取来源,StringWriter则可以作为写入目的地,最后用toString()取得所有写入的字符组成的字符串。在启动JVM时,可以用-Dfile.encoding来指定FileReader、FileWriter所使用的编码。
2.字符处理装饰器
如果串流处理的字节数据,实际上代表某些字符的编码数据,而你想要将这些字节数据转换为对应的编码字符,可以使用InputStreamReader、OutputStreamWriter对串流数据打包,在建立InputStreamReader与OutputStreamWriter时,可以指定编码,如果没有指定编码,则以JVM启动时所获取的默认编码来做字符转换。PrintStream与PrintWriter使用上极为相似,不过除了可以对OutputStream打包之外,PrintWriter还可以对Writer进行打包,提供print()、println()、format()等方法。
第十一章
一、线程
1.线程简介
到目前为止介绍过的各种范例都是单线程程序,也就是启动的程序从main()程序进入点开始至结束只有一个流程,有时候需要设计程序可以拥有多个流程,也就是所谓的多线程程序。
结果为:
在Java中,如果想在main()以外独立设计流程,可以撰写类操作java.lang.Runnable接口,流程的进入点是操作在run()方法中,例如
结果为:
在这个程序中,主线程执行main()定义的流程,main()定义的流程中建立了tortoiseThread与hareThread两个线程,这两个线程会分别执行Tortoise与Hare的run()定义的流程,要启动线程执行指定流程,必须调用Thread实例的start()方法。
结果为:
2.Thread与Runnable
从抽象观点与开发者的角度来看,JVM是台虚拟计算机,只安装了一颗称为主线程的CPU,可执行main()定义的执行流程。如果想要为JVM加装CPU,就是创建Thread实例,要启动额外CPU就是调用Thread实例的start()方法,额外CPU执行流程的进入点,可以定义在Runnable接口的run()方法中。除了将流程定义在Runnable的run()方法中,另一个撰写多线程程序的方式,就是继承Thread类,重新定义run()方法。操作Runnable接口的好处就是较有弹性,你的类还有机会继承其他类。若继承了Thread,那该类就是一种Thread,通常是为了直接利用Thread中定义的一些方法,才会继承Thread来操作。
3.线程生命周期
Daemon线程:主线程会从main()方法开始执行,直到main()方法结束后停止JVM。如果主线程中启动了额外线程,默认会等待被启动的所有线程都执行完run()方法才中止JVM。如果一个Thread被标示为Daemon线程,在所有的非Daemon线程都结束时,JVM会自动终止。从main()方法开始的就是一个非Daemon线程,可以使用setDaemon()方法来设定一个线程是否为Daemon线程。还可以使用isDaemon()方法判断线程是否为Daemon线程。
Thread基本状态图:在调用Thread实例start()方法后,基本状态为可执行(Runnable)、被阻断(Blocked)、执行中(Running)。实例化Thread并执行start()方法后,线程进入Runnable状态,此时线程尚未真正开始执行run()方法,必须等待排班器排入CPU执行,线程才会执行run()方法,进入Running状态,线程看起来像是同时执行,但事实上,同一个时间点,一个CPU还是只能执行一个线程,只是CPU会不断切换线程,且切换动作很快,所以看起来像是同时执行。线程有优先权,可用Thread的setPriority()方法设定优先权。最小值为1,最大值为10,默认是5。数字越大,优先权越高,排班器越优先排入CPU,如果优先权相同,则输流执行。运用多线程,当某线程进入Blocked时,让另一线程排入CPU执行,避免CPU闲下来,经常是改进效能的方式之一。
安插线程:如果A线程正在运行,流程中允许B线程加入,等到B线程执行完毕后再继续A线程流程,则可以使用join()方法完成这个需求。
有时候加入的线程可能处理太久,你不想无止境等待这个线程工作完毕,则可以在join()时指定时间,如join(10000),这表示加入成为流程的线程至多可处理10000毫秒,也就是10秒,如果加入的线程还没执行完,就不理它了。
停止线程:线程完成run()方法后,就会进入Dead,进入Dead(或已经调用过start()方法)的线程不可以再次调用start()方法,否则会抛出错误。Thread的resume()、suspend()、destroy()都不建议使用。
4.关于ThreadGroup
每个线程都属于某个线程群组,如果没有指定,则归入产生该子线程的线程群组,也可以自行指定线程群组,线程一旦归入某个群组,就无法再更换。ThreadGroup的某些方法,可以对群组中所有线程产生作用。如果想要一次取得群组中所有线程,可以使用enumerate()方法。activeCount()方法取得群组的线程数量。
5.synchronized与volatile
线程存取同一对象资源时会引发竞速情况,我们称这种为不具备线程安全的类。每个对象都会有个内部锁定,或称为监控锁定,被标示为synchronized的区块将会被监控,任何线程要执行synchronized区块都必须先取得指定的对象锁定。Java的synchronized提供的是可重入同步,也就是线程取得某对象锁定后,若执行过程中又要执行synchronized,尝试取得锁定的对象来源又是同一个,则可以直接执行。synchronized要求达到的所标示区块的互斥性与可见性。在Java中对于可见性的要求,可以使用volatile达到变量范围。在变量上声明volatile,表示变量是不稳定的,易变的。也就是可能在多线程下存取,这保证变量的可见性,也就是若有线程变动了变量值,另一线程一定可看到变更。被标示为volatile的变量,不允许线程快取,变量值的存取一定是在共享内存中进行。
6.等待与通知
wait()、notify()、notifyAll()是object定义的方法,可以通过这3个方法控制线程释放对象的锁定,或者通知线程参与锁定竞争。若调用锁定对象的wait()方法,线程会释放对象锁定,并进入对象等待集合从而处于阻断状态,其他线程可以竞争对象锁定,取得锁定的线程可以执行synchronized范围的程序代码。放在等待集合的线程不会参与CPU排班,wait()可以指定等待时间,时间到之后线程会再次加入排班。如果指定时间0或不指定,则线程会持续等待,直到被中断(调用interrupt())或是告知(notify())可以参与排班。
二、并行API
1.Lock、ReadWriter与Condition
如果需要的是线程池、读写锁等高级操作。从JDK5之后提供了java.util.concurrent包,可基于其中的API建立更稳固的并行应用程序。java.util.concurrent.looks包中提供Lock、ReadWriter与Condition接口以及相关操作类,可以提供类似 synchronized、 wait()、notify()、notifyAll()的作用,以及更多高级功能。 使用Lock:Lock接口主要操作类之一为ReentrantLock,可以达到synchronized的作用。 Lock接口还定义了tryLock()方法,如果线程调用tryLock()可以取得锁定会返回true,若无法取得锁定并不会发生阻断,而是返回false,依次可以解决死结问题。 使用ReadWriter:如果有两个线程都想调用get()与size()方法,由于锁定的关系,其中一个线程只能等待另一个线程解除锁定,无法两个线程同时调用get()与size()方法,然而这两个方法都只是读取对象状态,并没有变更对象状态,如果只是读取操作,可允许线程同时并行的话,那对读取效率将会有所改善。ReadWriter接口定义了读取锁定与写入锁定,可以使用readLock()、writeLock()方法返回Lock操作对象。
使用StampedLock:ReadWriteLock在没有任何读取或写入锁定时,才可以取得写入锁定。这可用于实现悲观读取。JDK8中新增了StampedLock类,可支持乐观读取。也就是说若读取线程很多,写入线程很少的情况下,你可以乐观的认为,写入与读取同时发生的机会甚少,因此不悲观的使用完全的读取锁定,程序可以查看数据读取之后,是否遭到写入线程的变更,再采取后续的措施。
使用Condition:Condition接口用来搭配Lock。调用Condition的await()将会使线程进入Condition的等待集合,要通知等待集合中的一个线程,则可以调用signal()方法,如果要通知所有等待集合中的线程,则用signalAll()方法。Condition的await()、signal()、signalAll()方法均可视为Object的wait()、notify()、notifyAll()方法的对应。
2.使用Executor
Runnable用来定义可执行流程与可使用数据,Thread用来执行Runnable。从JDK5开始,定义了java.util.concurrent.Executor接口,目的是将Runnable的指定与实际如何执行分离开来。
在Java SE API中,像线程池这类服务的行为,实际上是定义在Executor的子接口java.util.concurrent.ExecutorService中。如果需要线程池的功能,则可以使用java.util.concurrent.ThreadPoolExecutor。根据不同线程池的需求,ThreadPoolExecutor拥有数种不同构造函数可供使用。ExecutorService的shutdown()方法会在指定执行的Runnable都完成后,将ExecutorService关闭,另一个shutdownNow()方法则可以立即关闭ExecutorService,尚未被执行的Runnable对象会以List返回。 Future经常与Callable搭配使用,Callable的作用与Runnable类似,可让你定义想要执行的流程,不过Runnable的run()方法无法返回值,也无法抛出受检异常,然而Callable的call()方法可以返回值,也可以抛出受检异常。ScheduledExecutorService为ExecutorService的子接口,顾名思义,可以让你进行工作排程。Schedule()方法用来排定Runnable或者Callable实例延迟多久后执行一次,并返回Future子接口ScheduledFuture的实例。Future的另一操作类java.util.concurrent.ForkJoinTask及其子类只要目的是解决分而治之的问题。所谓分而治之的问题,就是指这些问题的解决,可以分解为性质相同的子问题,子问题还可以再分解为更小的子问题,将性质相同的子问题解决并收集运算结果,在多处理器的环境中,每个处理器若分配一个线程,每个线程分别执行一个子任务。ForkJoinPool与其他的ExecutorService操作不同的地方在于,它实现了工作窃取演算。其建立的线程如果完成手边任务,会尝试寻找并执行其他任务建立的子任务,让线程保持忙碌状态,有效利用处理器的能力。
3.并行Collection简介
CopyOnWriteArrayList操作了List接口,顾名思义,这个类的实例在写入操作时,内部会建立新数组,并复制原有数组索引的参考,然后在新数组上进行写入操作,写入完成后,再将内部原参考旧数组的变量参考至新数组。对于一个很少进行写入操作,而使用迭代器频繁的情景下,可以使用CopyOnWriteArrayList提高迭代器操作的效率。BlockingQueue是Queue的子接口,新定义了put()与take()等方法,线程若调用put()方法,在队列已满的情况下会被阻断,线程若调用了take()方法,在队列为空的情况下会被阻断。ConcurrentMap是Map的子接口,其定义了putIfAbsent()、remove()、replace()等方法。这些方法都是原子操作。putIfAbsent()在键对象不存在ConcurrentMap中时,才可置入键/值对象,否则返回键对应的值对象。remove()只有在键对象存在,且对应的值对象等于指定的值对象,才将键/值对象移除。replace()有两个版本,其中一个版本是只有在键对象存在,且对应的值对象等于指定的值对象,才将值对象置换,另外一个版本是在键对象存在时,将值对象置换。
教材学习中的问题和解决过程
其实感觉还好,就像娄老师所说的,第四五六七章的内容才是java核心内容,比较抽象难懂。之后的内容都是介绍各种API的应用,都是活生生的例子,比较具体,如果觉得难那是因为对这部分知识感到陌生,不熟悉。自己首先理清头绪,然后多看几遍书,多敲几遍代码。感觉立马就上来了!要讲究科学的学习方法~不要盲目!!!
具体代码已全部托管,在这里就不一一赘述了。
代码调试中的问题和解决过程
不明白书上334页的代码为什么先执行threadB。 后来才发现,程序启动后主线程就开始,在主线程中新建threadB,并在启动threadB后 ,将之加入主线程流程,所以threadB会先执行完毕,然后主线程才会继续原本的流程。
最后运行结果如下:
其他(感悟、思考等,可选)
虽说这周又是自学两章,知识点比较多,但还好,没什么难点,就像娄老师之前说的,java的核心知识与难点之前都已经学完了,后面的章节全都是介绍一些类的应用。看第一遍教材的时候肯定觉得陌生,难以接受。这是个过程,很正常。在不断的学习中,我也在不断的寻找适合自己的好的学习方法。看第一遍教材只是想有个大概的了解,头脑里勾画出一个轮廓。然后看第二遍才是逐渐理解与体会,往轮廓里填内容,这时不能只看书,还要结合书上的代码,自己还要主动敲代码,主动发现问题。 第三遍是梳理知识点也是回忆,将大脑与知识相融合。这样下来对知识绝对会有进一步的掌握!自身也会有质的飞跃!要学会抓住重点,把力量用在刀刃上,寻找乐趣,保持激情,提高学习效率!我是带着目的去看教材的,不是盲目的。我后来发现这点尤其重要!我知道第十章和第十一章也是介绍一些类的应用,于是我采取和上周一样的办法,一边看书,一边总结,看书上总共介绍了多少种API,每一种API的架构是什么,每一种API的作用与注意事项是什么。就这样有系统的去学习,感觉效率十分高!而且头脑思绪清晰。其实这些知识不是难,我们只是感到陌生而已。同学们有了畏难情绪和厌学情绪,当然就学不进去了,还谈什么效率!这两章的知识不像之前对象、封装、继承那些基本概念那么抽象难懂,都是活生生的具体的例子,接受起来其实也挺快的。娄老师说的很对,重要的不是要你学多少java知识,而是通过不断的学习过程,来总结出一套适合自己的良好的学习方法,这将受用一生。当然不同的人肯定情况不一样,适合自己的才是最好的。我这周最大的收获就是真正体会到,学习也要讲科学,不要盲目。时间用得多,不一定就学得好。找到属于自己的学习方法,提高效率!这比一切都重要!调整一下自己的心态吧,任何事情不要有畏难情绪,万事开头难,只要是对的,就坚持!最终一定会受益匪浅!!!
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
目标 | 3500行 | 20篇 | 300小时 | |
第一周 | 120/120 | 1/1 | 14/14 | |
第二周 | 340/460 | 1/2 | 14/28 | |
第三周 | 200/660 | 1/3 | 14/42 | |
第四周 | 320/980 | 1/4 | 14/56 | |
第五周 | 280/1260 | 1/5 | 14/70 | |
第六周 | 478/1738 | 2/7 | 16/86 |