20145326 《Java程序设计》第7周学习总结

20145326 《Java程序设计》第7周学习总结

教材学习内容总结

第十二章

一、认识Lambda语法

1.Lambda语法概览

Arrays的sort()方法可以用来排序,只不过你要告诉它两个元素比较时顺序是什么,sort()规定你得操作java.util.Comparator来说明这件事,我们可以通过byLength()来让排序的意图更清楚,只是操作Comparator时的匿名类时依旧冗长,有太多重复信息,如果使用JDK8的话,你可以使用Lambda特性去除重复的信息。Lambda不只是匿名类的语法蜜糖,目前先集中介绍其重复性的去除和可读性的改善。如果你在许多地方都会有按字符串长度排序的需求,你会怎么做?如果是同一个方法内,我打算用一个byName局部变量,如果是类中多个方法间要共享,那就用一个byName的数据成员吧!因为byName要参考的实例没有状态问题,因而声明为static比较合适,如果要在多个类之间共享,那么就设定为public static。JDK8提供了方法参考的特性,在Java中引入Lambda的同时,与现有API维持兼容性是主要考虑之一,方法参考的特性在重用现有API上扮演了重要角色,重用现有操作可避免到处写下Lambda表达式,不仅如此,也会让程序代码更为清楚。

2.Lambda表达式与函数接口

一个Lambda表达式可以拆为两部分,等号右边是Lambda表达式,等号左边是作为Lambda表达式的目标类型。区块可以由数个描述句组成,不过基本上不建议如此使用,在使用Lambda表达式时,尽量使用简单的表达式会比较好,如果你的操作比较复杂,可以考虑方法参考等其他方式。Lambda表达式中,即使不使用任何参数,也必须写下括号。在只有Lambda表达式的情况下,参数的类型必须写出来,如果有目标类型的话,在编译程序可推断出类型的情况下,就可以不写出Lambda表达式的参数类型。Lambda表达式本身是中性的,不代表任何类型的实例。同样的Lambda表达式,可以用来表示不同目标类型的对象操作。JDK8的Lambda并没有导入新类型来作为Lambda表达式的类型,而是就现有的interface语法来定义函数接口,作为Lambda表达式的目标类型,函数接口就是接口,但要求仅具单一抽象方法,许多现存的接口都是这种接口,像是标准API中的Runnable、Callable、Comparator等,都只定义了一个方法。不过在JDK8中有时会难以直接分辨接口是否为函数接口,因为JDK8对interface语法做了演进,允许有默认方法,而接口可能继承其他接口、重新定义了某些方法等,这些都会使得确认接口是否为函数接口更为困难。如果接口使用了@FunctionalInterface来标注,而本身并非函数接口的话,就会引发编译错误。

3.Lambda遇上this与final

 

运行结果:

在上面的范例中,因为是Hello类包围了Lambda表达式,Lambda表达式可以参考类范畴中的名称,范例中定义了Hello类的toString()返回“Hello,world!”字符串,因而执行时才会显示两次的“Hello,world!”。在JDK8出现前,如果要在匿名内部类中存取局部变量,则该局部变量必须是final,否则会发生编译错误,而在JDK8中,如果变量本身等效于final局部变量,也就是说,如果变量不会在匿名类中有重新指定的动作,就可以不用加上final关键词。注意!在Lambda表达式中,不能改变被捕获的局部变量值,JDK8特意禁止你在Lambda中修改局部变量的值,因为JDK8想要采用Lambda的理由之一,是想进一步支持并行程序设计,Lambda表达式中可变动的局部变量,通常也表示在并行程序中,可能必须处理同步锁定问题,JDK8以禁止你在Lambda中修改局部变量值来避免这类的问题。

4.方法与构造函数参考

当你临时想要为函数接口定义操作时,Lambda表达式确实是很方便,然而有时候,你会发现某些静态方法的本身操作流程,与你自行定义的Lambda表达式根本就是相同,JDK8考虑到这种情况,Lambda表达式只是定义函数接口操作的一种方式,除此之外,只要静态方法的方法签署中,参数与返回值定义相同,也可以使用静态方法来定义函数接口操作。这样的特性在JDK8中称为方法参考,可以改善可读性。函数接口操作也可以参考类上定义的非静态方法,函数接口会试图用第一个参数作为方法接收者,而之后的参数依序作为被参考的非静态方法之参数。

5.接口默认方法

JDK8希望这些API具备面向对象程序设计风格,在撰写程序代码时也能更为顺畅。JDK8采取的策略是,直接演化interface的语法,在JDK8中,interface定义时可以加入默认操作,或者称为默认方法,默认方法令接口看起来像是有抽象方法的抽象类,不过不同点在于,默认方法中不能使用数据成员。因为接口本身不能定义数据成员,也就是默认方法中不能有直接变更状态的流程。JDK8出现前不让接口拥有默认方法是有原因的,因为操作接口是广义的多重继承,接口没有操作时,类与接口继承时的方法来源在判断上就会单纯许多,接口在JDK8中允许有默认操作,引入了强大的威力,也引入了更多的复杂度。就如同子类可以继承父类操作,接口也可以被继承,抽象方法或默认方法都会被继承下来,在子接口中再以抽象方法重新定义一次父接口已定义的抽象方法,通常是为了文件化,这是过去经常看到的实践(practice),一旦接口中可以定义默认方法,辨别操作版本时就有许多需要注意的地方,如果父接口中有个默认方法,子接口中再度声明与父接口默认方法相同的方法命名,但没有写出default,也就是没有方法操作,那么子接口中该方法就直接重新定义了父类中的默认方法操作为抽象方法,如果有两个父接口都定义了相同方法命名的默认方法,那么会引发冲突。解决方式是明确重新定义方法,无论是重新定义为抽象还是默认方法。如果子类继承了父类同时操作某接口,而父类中的方法与接口中的默认方法具有相同方法命名,则采用父类的方法定义。简单来说,类中的定义优先于接口中的定义,如果有重新定义,就以重新定义的为主,必要时使用接口与super指定采用哪个默认方法。

二、Functional与Stream API

1.使用Optional代替null

null的最根本问题就在于语意含糊不清,就字面来说,null可以是“不存在”、“没有”、“无”、“空”的意思,因此在应用时,总是有人摸棱两可,也就让开发者有了各自解释的空间。当开发者因为某个原因,不假思索的放个null时,然后用户就总是忘了检查null,引发各种可能的错误。我们要确认使用null的时机与目的,并使用明确的语意。如果不想返回null,可以返回Optional实例,调用方法时如果返回类型是Optional,应该立即想到它可能包含也可能不包含值。要建立Optional实例有几个静态方法,使用of()方法可以指定非null值建立Optional实例,使用empty()方法可以建立不包含值的Optional实例。在Optional没有包含值的情况下,就会直接抛出java.util.NoSuchElementException,这实现了速错的概念。一个较好的方式是使用orElse()方法,指定值不存在时的替代值。过去许多链接库中使用了不少null,这些链接库无法说改就改,可使用Optional的ofNullable()来衔接链接库中会返回null的方法,使用ofNullable()方法时,若指定了非null值就会调用of()方法,指定了null值就会调用empty()方法。

2.标准API的函数接口

JDK8定义的通用函数接口,基本上放置于java.util.function套件之中,就行为来说,基本上可以分为Consumer、Function、Predicate与Supplier四个类型。如果需要的行为是接受一个自变量,然后处理后不返回值,就可以使用Consumer接口。Consumer接口主要是接受单一对象实例作为自变量,对于基本类型int、long、double,另外有IntConsumer、LongConsumer、DoubleConsumer三个函数接口;对于接受两个对象实例作为自变量的接口则为BiConsumer,另外还有ObjIntConsumer、ObjLongConsumer、ObjDoubleConsumer,这三个函数接口第一个参数接受对象实例,第二个参数分别接受int、long、double。如果需要的是接受一个自变量,然后以该自变量进行计算后返回结果,就可以使用Function接口。Function的子接口为UnaryOperator,特殊化为参数与返回值都是相同类型,虽然JDK8仍不支持运算符重载,不过这个命名显然源于某些语言中,运算符也是个函数的概念。如果接受一个自变量,然后只返回boolean值,也就是根据传入的自变量直接论断真假的行为,就可以使用Predicate函数接口。如果需要的行为是不接受任何自变量,然后返回值,那可以使用Supplier函数接口。

3.使用Stream进行管道操作

可以看出最大的差别就是没有用到for循环与if判断式,以及使用了管道操作风格,而功能上也有所差异,如果读取的文件很大,第二个程序片段会比第一个程序片段更加有效。功能上的差异性在于,第一个程序片段的Files.readAllLines()方法返回的是List实例,其中包括了文档中所有行,如果第一行就符合指定的条件了,那后续的行读取就是多余的,第二个程序片段的lines()方法实际上没有进行任何一行的读取,filter()也没有做任何一行的过滤,直到调用findFirst()时,filter()指定的条件才会真正去执行,而此时才会要求lines()返回的Stream进行第一行读取,如果第一行就符合,那后续的行就不会再读取。一个管道包括了:来源、零或多个中间操作、一个最终操作。Stream只能迭代一次,重复对Stream进行迭代,会引发错误。

4.进行Stream的reduce与collect

 

运行结果:

在一组数据中,按条件求得一个数,或将一组数据按条件收集至另一个容器,程序设计中不少地方都存在这类需求,使用循环解决这类需求,也是许多开发者最常用的方法。在这个循环结构里,实际上有个步骤 ,它将一组数据逐步取出削减,然后通过指定运算以取得结果的结构,JDK8将这个流程结构通用化,定义了reduce()方法来达到自定义运算需求。reduce()的Lambda表达式必须接受两个自变量,第一个自变量为访问该组数据上一元素后的运算结果,第二个自变量为目前访问元素,Lambda表达式本身就是你原先在循环中打算进行的运算。reduce()若没有指定初值,就会试着使用该组数据中第一个元素作为第一次调用Lambda表达式时的第一个自变量值,因为考虑到数据组可能为空,因此reduce()不指定初值的版本,会返回OptionInt。当collect()需要收集对象时,会使用第一个Lambda来取得容器对象,这相当于Collector的supplier()的作用,第二个Lambda定义了如何收集对象,也就是Collector的accumulator()的作用,在使用具有并行处理能力的Stream时,有可能会使用多个容器对原数据组进行分而治之,当每个小任务完成时,该如何合并,就是第三个Lambda要定义的。

5.关于flatMap()方法

在程序设计中有时会出现巢状或瀑布式的流程,就结构来看每一层运算极为类似,只是返回的类型不同,很难抽取流程重用。方法可能有可能没有值时,不建议使用null作为没有值的代表。Optional的flatMap()这个名称很令人迷惑,flatMap()就像是从盒子取出另一盒子,Lambda表达式指定了前一个盒子中的值与下一个盒子之间的转换关系,因为判断是否有值的运算情景被隐藏了,所以用户可以明确指定感兴趣的特定运算。map()方法与flatMap()方法的差别在于,map()方法操作中,对mapper.apply(value)的结果使用了Optional.ofNullable()方法,因此有办法持续处理null的情况。

三、Lambda与并行处理

1.Stream与平行化 stream()与parallelStream()两个方法,前者代表循环处理,后者代表并行处理,想知道Stream是否为并行处理,可以调用isParallel()来得知。要注意!使用了ParallelStream()不代表一定会并行处理而使得执行必然变快。必须思考处理过程是否能够分而治之而后合并结果。在collect()操作时若想有平行效果,必须满足三个条件:Stream必须有并行处理能力、Collector必须有Collector.Characteristics.CONCURRENT特性、Stream是无序的或者是Collector具有Collector.Characteristics.UNORDERED特性。当API处理小问题时,你不应该进行干扰,且最好一次只做一件事。

2.使用CompletableFuture

如果你要异步读取文本文件,在文档读取完后做某些事,可以使用ExecutorService来Submit()一个Runnable对象。这种异步操作使用的回调风格,在每次回调中若又再度进行异步操作及回调,很容易写出回调地狱,造成可读性不佳。CompletableFuture的静态方法supplyAsync()接受Supplier实例,可指定异步执行任务,它会返回CompletableFuture实例。

第十三章

一、认识时间与日期

1.时间的度量

在正式认识Java提供了哪些时间处理API之前,得先来了解一些时间、日期的历史问题,这样你才会知道,时间日期确实是个很复杂的问题,而使用程序来处理时间日期,也不仅仅是使用API的问题。

a.格林威治标准时间:格林威治标准时间简称GMT时间,一开始是参考自格林威治皇家天文台的标准太阳时间,格林威治标准时间的正午是太阳抵达天空最高点之时,格林威治标准时间常被不严谨的当成是UTC时间。

b.世界时:世界时是借由观测远方星体跨过子午线而得,也称UT,这会比观察太阳来得准确一些。1972年引入UTC之前,GMT与UT是相同的。

c.国际原子时:虽然观察远方星体会比观察太阳来得准确,不过UT基本上仍受地球自转速度影响而会有所误差。1967年定义的国际原子时(TAI),将秒的国际单位定义为铯原子辐射振动9192631770周耗费的时间,时间从UT的1958年开始同步。

d.世界协调时间:由于基于铯原子振动定义的秒长是固定的,然而地球自转会越来越慢,这会使得实际上 TAI时间会不断超前基于地球自转的UT系列时间,为了保持TAI与UT时间不要差距过大,因而提出了具有折衷修正版本的世界协调时间(UTC)。

e.Unix时间:Unix系统的时间表示法,定义为UTC时间1970年1月1日00:00:00为起点而经过的秒数,不考虑闰秒修正,用以表达时间轴上某一瞬间。

f.epoch:某个特定时代的开始,时间轴上某一瞬间。例如java.util.Date封装的时间信息,就是January 1,1970,00:00:00 GMT经过的毫秒数,可以简称为epoch毫秒数。

就目前来说,即使标注为GMT,实际上谈到的时间指的是UTC时间。秒的单位定义是基于TAI,也就是铯原子辐射振动次数。Unix时间是1970年1月1日00:00:00为起点而经过的秒数,不考虑闰秒。

2.年历简介

度量时间是一回事,表达日期又是另一回事,前面谈到的时间起点,都是使用公历,中文世界又常称为阳历,在谈到公历之前,先来看看其他的历法。儒略历是现今公历的前身,用来取代罗马历。儒略历修正了罗马历隔三年设置一闰年的错误,改采四年一润。格里高利历改革了儒略历,将儒略历1582年10月4日星期四的隔天,定为格里高利历1582年10月15日星期五。在一些相对来说较新的时间日期API应用场合中,你可能会看到ISO 8601,严格来说ISO 8601并非年历系统,而是时间日期表示方法的标准,用以统一时间日期的数据交换格式,ISO 8601在数据定义上大部分与格里高利历相同,不过还是有微小差别。在ISO 8601的定义中,19世纪是指1900年至1999年(包含该年),而格里高利历的19世纪是指1801年至1900年(包含该年)。

3.认识时区

从地理上来说,由于地球是圆的,基本上一边白天另一边就是夜晚,为了让人们对时间的认知符合作息,因而设置了UTC偏移,大致上来说,经度每15度是偏移一个小时,考虑了UTC偏移的时间表示上,通常会标识Z符号。不过有些国家的领土横跨的经度很大,一个国家有多个时间反而造成困扰,因而不采取每15度偏移一小时的做法,像美国仅有4个时区,而中国,印度只采用单一时区。一年的毫秒数绝对不是单纯的 365X24X60X60X1000,更不要基于这类错误的观念来进行时间与日期运算。

二、认识Date与Calendar

1.时间轴上瞬间的Date

如果想要取得系统时间,方法之一是使用System.currentTimeMillis()方法,返回的是long类型整数,代表1970年1月1日0时0分0秒0毫秒至今经过的毫秒数,也就是时间起点与前面谈到的Unix时间起点是相同的。不过以此方法取得的是机器的时间观点,代表着时间轴上某一瞬间,这一长串的epoch毫秒数不是人类的时间观点,对人类来说没有阅读上的意义。Date有两个构造函数可以使用,一个可使用epoch毫秒数构建,另一个为无自变量构造函数,内部亦是使用System.currentTimeMillis()取得epoch毫秒数,调用getTime()可取得内部保存的epoch毫秒数值。getTime()之外的getXXX()方法都废弃了,setTime()之外的setXXX()方法也都废弃了。Date实例基本上建议只用来当作时间轴上的某一瞬间,也就是1970年1月1日0时0分0秒至今经过的毫秒数,其他对时间日期字段的设定与取得,建议通过Calendar来执行。

 

运行结果:

2.格式化时间日期的DateFormat

有关字符串时间格式的处理,职责落到了java.text.DateFormat身上,DateFormat是个抽象类,其操作类是java.text.SimpleDateFormat,你可以直接构建SimpleDateFormat实例,或是使用DateFormat的getDateInstance()、getTimeInstance()、getDateTimeInstance()等静态方法。

 

结果:

SimpleDateFormat还有个parse()方法,可以按构建SimpleDateFormat时指定的格式,将指定的字符串剖析为Date实例。

 

结果:

3.处理时间日期的Calendar

Date现在建议作为时间轴上的瞬时代表,要格式化时间日期则通过DateFormat,如果想要取得某个时间日期信息,或者是对时间日期进行操作,可以使用Calendar实例。Calendar是个抽象类,java.util.GregorianCalendar是其子类,操作了儒略历与格里高利历的混合历,通过Calendar的getInstance()取得的Calendar实例,默认就是取得GregorianCalendar实例。如果想要比较两个Calendar的时间日期先后,可以使用after()或before()方法。

结果:

4.设定TimeZone

前面在使用Calendar时,并没有使用时区信息,这会使用默认时区,你可以使用java.util.TimeZone的getDefault()来取得默认时区信息。 

 

结果:

 

 

运行结果:

三、JDK8新时间日期API

1.机器时间观点的API

Date实例真正代表的并不是日期,最接近的概念应该是时间轴上特定的一瞬间,时间精度是毫秒,也就是UTC时间1970年1月1日0时0分0毫秒至某个特定瞬时的毫秒差。Date名称看来像是人类的时间概念,实际上却是机器的时间概念。真正可靠的信息只有内含的epoch毫秒数。所以如果你取得Date实例,下一步获取时间信息应该是通过Date的getTime()取得epoch毫秒数。JDK8新时间日期处理API中,最重要的就是清楚的将机器对时间的概念与人类对时间的概念分隔开来。

2.人类时间观点的API

对于片段的日期时间,JDK8新时间与日期API有LocalDateTime()、LocalDate()、LocalTime()等类来定义,这些类基于ISO 8601年历系统,是不具时区的时间与日期定义。

 

 

结果:

在新的时间与日期API中,UTC偏移量与时区的概念是分开的,offsetDateTime单纯代表UTC偏移量,使用ISO 8601。如果只想表示2014年,可以使用Year,如果想表示2014/5,可以使用YearMonth,如果只想表示5月,可以使用Month,如果想表示5/4,可以使用MonthDay,其中Month是enum型,如果你想要取得代表月份的数字,不要使用oridinal()方法,因为oridinal()是enum在定义时的顺序,从0开始,想要取得代表月份的数要通过getValue()方法。

 

 

结果:

3.对时间的运算

 

 

结果:

 

period与Duration乍看有些难区别,period是日期差,between()方法只接受LocalDate,不表示比“日”更小的单位。然而Duration是时间差,between()方法可以接受LocalDateTime()、LocalDate()、LocalTime(),不表示比“天”更大的单位。

教材学习中的问题和解决过程

娄老师说本周任务是重点学习13章,12章如果没有时间可以不看。大部分同学第一反应就是“肯定没时间”。其实感觉还好,我就按着自己的节奏走,没想到一下子就顺利的将12章与13章内容都学了(不过重点还是放在13章的)。之前娄老师说过,java的核心内容是封装、继承、多态那部分知识,确实比较抽象难懂。之后的内容都是介绍各种API的应用,都是活生生的例子,比较具体,如果觉得难那是因为对这部分知识感到陌生,不熟悉。自己首先理清头绪,不懂的基础知识多看几遍书,然后再多敲几遍代码,仔细思考总结,将代码与知识点结合,感觉立马就上来了!要讲究科学的学习方法~不要盲目!!!

问题: 如何计算一个程序的运行时间?

解决

(1)以毫秒为单位计算:

long startTime=System.currentTimeMillis(); //获取开始时间

 doSomeThing(); //测试的代码段

 long endTime=System.currentTimeMillis(); //获取结束时间

 System.out.println("程序运行时间: "+(end-start)+"ms");

(2)以纳秒为单位计算:

long startTime=System.nanoTime(); //获取开始时间

  doSomeThing(); //测试的代码段

  long endTime=System.nanoTime(); //获取结束时间

  System.out.println("程序运行时间: "+(end-start)+"ns");

代码调试中的问题和解决过程

书上435页的代码为什么要调用clone()?

解决: 避免调用yearsBetween()、datsBetween()之后传入的Calendar自变量被修改,两个方法都对第一个自变量进行了clone()复制对象的动作。

执行结果如下:

 

代码托管:

其他(感悟、思考等,可选)

虽说我这周又是自学两章,知识点比较多,但还好,没什么难点,就像娄老师之前说的,java的核心知识与难点之前都已经学完了,后面的章节全都是介绍一些类的应用。看第一遍教材的时候肯定觉得陌生,难以接受。这是个过程,很正常。在不断的学习中,我也在不断的寻找适合自己的好的学习方法。看第一遍教材先有个大概的了解,头脑里勾画出一个轮廓。然后看第二遍才是逐渐理解与体会,往轮廓里填内容,这时不能只看书,还要结合书上的代码,自己还要主动敲代码,主动发现问题。 第三遍是梳理知识点也是回忆,将大脑与知识相融合。这样下来对知识绝对会有进一步的掌握!自身也会有质的飞跃!要学会抓住重点,把力量用在刀刃上,寻找乐趣,保持激情,提高学习效率!现在的我是带着目的去看教材的,不是像以前那样盲目的。我后来发现这点尤其重要!我知道第12章和第13章跟之前一样,还是介绍一些类的应用,于是我采取和之前一样的办法,一边看书,一边总结,看书上总共介绍了多少种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  
第七周 425/2163 2/9 16/102  

posted on 2016-04-17 11:03  20145326蔡馨熠  阅读(363)  评论(4编辑  收藏  举报

导航