diff --git a/.github/ISSUE_TEMPLATE/contribution.md b/.github/ISSUE_TEMPLATE/contribution.md new file mode 100644 index 00000000..eecca209 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/contribution.md @@ -0,0 +1,22 @@ +--- +name: 贡献力量 +about: 请描述你需要贡献的具体方面 +--- + +## 我愿成为一名开源贡献者! + +### 申请翻译/校对 + + + * 翻译 + +### 具体章节/标题 + + +* 示例章节 + * 示例标题1 + * 示例标题2 + + + + diff --git a/.gitignore b/.gitignore index 4cb12d8d..851523cc 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,12 @@ _book *.epub *.mobi *.pdf + +# OSX junk files +.DS_Store +._* +*/.DS_Store +*/._* + +# IDEA +/.idea/* \ No newline at end of file diff --git a/GLOSSARY.md b/GLOSSARY.md deleted file mode 100644 index 9b18bbee..00000000 --- a/GLOSSARY.md +++ /dev/null @@ -1,10 +0,0 @@ - ## 词汇表 - -| 词汇 | 解释 | -| ----------------| ----------| -| **OOP** (*Object-oriented programming*) | 面向对象编程,一种编程思维模式和编程架构| -| **UML** (*Unified Modeling Language*) | 统一建模语言,类图 | -| **Aggregation** | 聚合,关联关系的一种,是强的关联关系| -| **Composition** | 组合,关联关系的一种,是比聚合关系强的关系 | -| **STL**(*the Standard Template Library*)| C++ 标准模板库| - diff --git a/Introduction.md b/Introduction.md deleted file mode 100644 index af91d14f..00000000 --- a/Introduction.md +++ /dev/null @@ -1,56 +0,0 @@ -# 译者的话 - -[![GitHub stars](https://img.shields.io/github/stars/lingcoder/OnJava8.svg?style=social&label=Star&)](https://github.com/lingcoder/OnJava8/stargazers)[![GitHub forks](https://img.shields.io/github/forks/lingcoder/OnJava8.svg?style=social&label=Fork&)](https://github.com/lingcoder/OnJava8/fork) - -本翻译项目的 Git 开源地址:[https://github.com/LingCoder/OnJava8](https://github.com/LingCoder/OnJava8) - -如果您在阅读本书的过程中有发现不明白或者错误的地方,请随时到项目地址发布issue或者fork项目后发布pr帮助译者改善!不胜感激! - - -## 书籍简介 - -* 本书原作者为 \[美\] [Bruce Eckel](https://github.com/BruceEckel),即(*Thinking in Java 4th Edition,2006*)的作者。 -* 本书是事实上的 *Thinking in Java 5th Edition*(*On Java 8,2017*)。 -* *Thinking in Java 4th Edition*基于 **JAVA 5**版本;*On Java 8*基于**JAVA 8**版本。 - - -## 贡献者 - -- 主译: LingCoder -- 参译: - - -## 翻译说明 - -1. 本书排版布局和翻译风格上参考了**阮一峰**老师的[中文技术文档的写作规范](https://github.com/ruanyf/document-style-guide) -2. 采用第一人称叙述。 -3. 由于中英行文差异,完全的逐字逐句翻译会很冗余啰嗦。所以本人在翻译过程中,去除了部分主题无关内容、重复描写。 -4. 译者在翻译中同时参考了谷歌、百度、有道翻译的译文以及《Java编程思想》第四版中文版的部分内容(对其翻译死板,生造名词,语言精炼度差问题进行规避和改正)。最后结合译者自己的理解进行本地化,尽量做到专业和言简意赅,方便大家更好的理解学习。 -5. 由于译者个人能力、时间有限,如有翻译错误和笔误的地方,还请大家批评指正! - - -## 如何参与 - - -如果你想对本书做出一些贡献的话 -可以在阅读本书过程中帮忙校对,找 bug 错别字等等 -可以提出专业方面的修改建议 -可以把一些不尽人意的语句翻译的更好更有趣 -对于以上各类建议,请以 issue 或 pr 的形式发送,我看到之后会尽快处理 -使用 MarkDown 编辑器,md 语法格式进行文档翻译及排版工作 -完成之后 Pull Request -如没问题的话,我会合并到主分支 -如果不太明白 md 的排版,可以把翻译好的内容发送给我,我代为排版并提交 -如还有其它问题,欢迎发送 issue,谢谢~ - - -## 开源协议 - -本项目基于 MIT 协议开源。 - - -## 联系方式 - -* E-mail : - - diff --git a/README.md b/README.md index ad8956a2..be0417e7 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,29 @@ # 《On Java 8》中文版 -## 书籍简介 +## 最新动态 -* 本书原作者为 \[美\] [Bruce Eckel](https://github.com/BruceEckel),即(*Thinking in Java 4th Edition,2006*)的作者。 -* 本书是事实上的 *Thinking in Java 5th Edition*(*On Java 8,2017*)。 -* *Thinking in Java 4th Edition*基于 **JAVA 5**版本;*On Java 8*基于**JAVA 8**版本。 +《ON JAVA 中文版》终于上市了!推荐大家去京东购买:https://u.jd.com/ZwXIEMn ,新书首发,价格也比较美丽。 +值得一提的是,为了与时俱进,作者也增补Java 11、Java17的相关内容,很多内容都非常贴合实际的开发场景,知识点非常细致,可以说覆盖了市面其他Java书90%的内容。 -## 快速阅读传送门 +随书配套视频也很精良!4位行业内的顶级大佬为这本书录制了配套教程,尤其是对初学者十分友好,重点知识都帮你划出来了。 -- GitHub 快速阅读:[进入目录](https://github.com/LingCoder/OnJava8/blob/master/SUMMARY.md) +—————————————— -- Gitee 快速阅读:[进入目录](https://gitee.com/lingcoder/OnJava8/blob/master/SUMMARY.md) +图灵要出On Java 8的中文版了! 非常感谢大家长久以来对本项目的支持和贡献,出于对原作者的敬意和对版权尊重,本项目将于2021年2月26日起闭源。 之后,我将作为On Java 8的特邀审读嘉宾,继续贡献自己的一份力量! -- GitBook 完整阅读:[进入Gitbook](https://lingcoder.gitbook.io/onjava8) - - -## 翻译进度 -- [x] 前言 -- [x] 简介 -- [x] 第一章 对象的概念 -- [x] 第二章 安装Java和本书用例 -- [x] 第三章 万物皆对象 -- [ ] 第四章 运算符 -- [x] 第五章 控制流 -- [x] 第十三章 函数式编程 -- [x] 附录:新IO - -- [ ] 待续······ +想要继续关注本书出版进度,请访问图灵社区:https://www.ituring.com.cn/book/2935 ## 一起交流 -交流群:721698221 OnJava8翻译交流( 点击图标即可加入 )
+点击链接加入群聊【Java技术流群】247457782 Java技术交流( 点击图标即可加入 )
加群时请简单备注下来源或说明
-QQGroupQRCode +QQGroupQRCode
- - -## 更新记录 - -- 2018年11月20日 初始化项目 - -- 2018年12月20日 前言,简介翻译完成 - -- 2019年01月01日 第一章 对象的概念翻译完成 - -- 2019年01月06日 第二章 安装Java和本书用例翻译完成 - -- 2019年03月14日 第三章 万物皆对象翻译完成 - -- 2019年03月17日 第五章 控制流翻译完成 - -- 2019年03月20日 第十三章 函数式编程翻译完成 - -- 2019年03月24日 附录:新IO翻译完成 - -## 原书作者 - -
-cover_small -
- -* 作者: Bruce Eckel -* ISBN: 9780981872520 - -## 贡献者 - -* 主译: LingCoder,LortSir -* 参译: -* 校对:nickChenyx - - -## 翻译说明 - -1. 本书排版布局和翻译风格上参考了**阮一峰**老师的[中文技术文档的写作规范](https://github.com/ruanyf/document-style-guide) -2. 采用第一人称叙述。 -3. 由于中英行文差异,完全的逐字逐句翻译会很冗余啰嗦。所以本人在翻译过程中,去除了部分主题无关内容、重复描写。 -4. 译者在翻译中同时参考了谷歌、百度、有道翻译的译文以及《Java编程思想》第四版中文版的部分内容(对其翻译死板,生造名词,语言精炼度差问题进行规避和改正)。最后结合译者自己的理解进行本地化,尽量做到专业和言简意赅,方便大家更好的理解学习。 -5. 由于译者个人能力、时间有限,如有翻译错误和笔误的地方,还请大家批评指正! - - -## 如何参与 - -如果你想对本书做出一些贡献的话 -可以在阅读本书过程中帮忙校对,找 bug 错别字等等 -可以提出专业方面的修改建议 -可以把一些不尽人意的语句翻译的更好更有趣 -对于以上各类建议,请以 issue 或 pr 的形式发送,我看到之后会尽快处理 -使用 MarkDown 编辑器,md 语法格式进行文档翻译及排版工作 -完成之后 Pull Request -如没问题的话,我会合并到主分支 -如果不太明白 md 的排版,可以把翻译好的内容发送给我,我代为排版并提交 -如还有其它问题,欢迎发送 issue,谢谢~ - - -## 友情链接 - -[Effective.Java.3rd.Edition 中文版](https://sjsdfg.github.io/effective-java-3rd-chinese/#/) - - - -## 开源协议 - -本项目基于 MIT 协议开源。 + ## 联系方式 -* E-mail : - - - - +- E-mail : diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index 8e845194..00000000 --- a/SUMMARY.md +++ /dev/null @@ -1,418 +0,0 @@ -# Summary - -* [Introduction](README.md) -* [译者的话](Introduction.md) -* [封面](book/00-On-Java-8.md) -* [前言](book/00-Preface.md) - * [教学目标](book/00-Preface.md#教学目标) - * [语言设计错误](book/00-Preface.md#语言设计错误) - * [测试用例](book/00-Preface.md#测试用例) - * [普及性](book/00-Preface.md#普及性) - * [关于安卓](book/00-Preface.md#关于安卓) - * [电子版权声明](book/00-Preface.md#电子版权声明) - * [版本说明](book/00-Preface.md#版本说明) - * [封面设计](book/00-Preface.md#封面设计) - * [感谢的人](book/00-Preface.md#感谢的人) - * [献礼](book/00-Preface.md#献礼) -* [简介](book/00-Introduction.md) - * [前提条件](book/00-Introduction.md#前提条件) - * [JDK文档](book/00-Introduction.md#JDK文档) - * [C编程思想](book/00-Introduction.md#C编程思想) - * [源码下载](book/00-Introduction.md#源码下载) - * [编码样式](book/00-Introduction.md#编码样式) - * [BUG提交](book/00-Introduction.md#BUG提交) - * [邮箱订阅](book/00-Introduction.md#邮箱订阅) - * [Java图形界面](book/00-Introduction.md#Java图形界面) - -### 正文 - -* [第一章 对象的概念](book/01-What-is-an-Object.md) - * [抽象](book/01-What-is-an-Object.md#抽象) - * [接口](book/01-What-is-an-Object.md#接口) - * [服务提供](book/01-What-is-an-Object.md#服务提供) - * [封装](book/01-What-is-an-Object.md#封装) - * [复用](book/01-What-is-an-Object.md#复用) - * [继承](book/01-What-is-an-Object.md#继承) - * [多态](book/01-What-is-an-Object.md#多态) - * [单继承](book/01-What-is-an-Object.md#单继承) - * [集合](book/01-What-is-an-Object.md#集合) - * [生命周期](book/01-What-is-an-Object.md#生命周期) - * [异常处理](book/01-What-is-an-Object.md#异常处理) - * [本章小结](book/01-What-is-an-Object.md#本章小结) -* [第二章 安装Java和本书用例](book/02-Installing-Java-and-the-Book-Examples.md) - * [编辑器](book/02-Installing-Java-and-the-Book-Examples.md#编辑器) - * [Shell](book/02-Installing-Java-and-the-Book-Examples.md#Shell) - * [Java安装](book/02-Installing-Java-and-the-Book-Examples.md#Java安装) - * [校验安装](book/02-Installing-Java-and-the-Book-Examples.md#校验安装) - * [安装和运行代码示例](book/02-Installing-Java-and-the-Book-Examples.md#安装和运行代码示例) -* [第三章 万物皆对象](book/03-Objects-Everywhere.md) - * [对象操纵](book/03-Objects-Everywhere.md#对象操纵) - * [对象创建](book/03-Objects-Everywhere.md#对象创建) - * [代码注释](book/03-Objects-Everywhere.md#代码注释) - * [对象清理](book/03-Objects-Everywhere.md#对象清理) - * [类的创建](book/03-Objects-Everywhere.md#类的创建) - * [程序编写](book/03-Objects-Everywhere.md#程序编写) - * [小试牛刀](book/03-Objects-Everywhere.md#小试牛刀) - * [编码风格](book/03-Objects-Everywhere.md#编码风格) - * [本章小结](book/03-Objects-Everywhere.md#本章小结) -* [第四章 运算符](book/04-Operators.md) - * [使用说明](book/04-Operators.md#使用说明) - * [优先级](book/04-Operators.md#优先级) - * [赋值](book/04-Operators.md#赋值) - * [算术运算符](book/04-Operators.md#算术运算符) - * [自动递增和递减](book/04-Operators.md#自动递增和递减) - * [关系运算符](book/04-Operators.md#关系运算符) - * [逻辑运算符](book/04-Operators.md#逻辑运算符) - * [字面值](book/04-Operators.md#字面值) - * [按位运算符](book/04-Operators.md#按位运算符) - * [移位运算符](book/04-Operators.md#移位运算符) - * [三元运算符](book/04-Operators.md#三元运算符) - * [字符串运算符](book/04-Operators.md#字符串运算符) - * [常见陷阱](book/04-Operators.md#常见陷阱) - * [类型转换](book/04-Operators.md#类型转换) - * [Java没有sizeof](book/04-Operators.md#Java没有sizeof) - * [运算符总结](book/04-Operators.md#运算符总结) - * [本章小结](book/04-Operators.md#本章小结) -* [第五章 控制流](book/05-Control-Flow.md) - * [true和flase](book/05-Control-Flow.md#true和flase) - * [if-else](book/05-Control-Flow.md#if-else) - * [迭代语句](book/05-Control-Flow.md#迭代语句) - * [for-in语法](book/05-Control-Flow.md#for-in语法) - * [return](book/05-Control-Flow.md#return) - * [break和continue](book/05-Control-Flow.md#break和continue) - * [臭名昭著的goto](book/05-Control-Flow.md#臭名昭著的goto) - * [switch](book/05-Control-Flow.md#switch) - * [switch字符串](book/05-Control-Flow.md#switch字符串) - * [本章小结](book/05-Control-Flow.md#本章小结) -* [第六章 初始化和清理](book/06-Housekeeping.md) - * [利用构造器保证初始化](book/06-Housekeeping.md#利用构造器保证初始化) - * [方法重载](book/06-Housekeeping.md#方法重载) - * [无参构造器](book/06-Housekeeping.md#无参构造器) - * [this关键字](book/06-Housekeeping.md#this关键字) - * [垃圾回收器](book/06-Housekeeping.md#垃圾回收器) - * [成员初始化](book/06-Housekeeping.md#成员初始化) - * [构造器初始化](book/06-Housekeeping.md#构造器初始化) - * [数组初始化](book/06-Housekeeping.md#数组初始化) - * [枚举类型](book/06-Housekeeping.md#枚举类型) - * [本章小结](book/06-Housekeeping.md#本章小结) -* [第七章 封装](book/07-Implementation-Hiding.md) - * [包的概念](book/07-Implementation-Hiding.md#包的概念) - * [访问权限修饰符](book/07-Implementation-Hiding.md#访问权限修饰符) - * [接口和实现](book/07-Implementation-Hiding.md#接口和实现) - * [类访问权限](book/07-Implementation-Hiding.md#类访问权限) - * [本章小结](book/07-Implementation-Hiding.md#本章小结) -* [第八章 复用](book/08-Reuse.md) - * [组合语法](book/08-Reuse.md#组合语法) - * [继承语法](book/08-Reuse.md#继承语法) - * [委托](book/08-Reuse.md#委托) - * [结合组合与继承](book/08-Reuse.md#结合组合与继承) - * [组合与继承的选择](book/08-Reuse.md#组合与继承的选择) - * [protected](book/08-Reuse.md#protected) - * [向上转型](book/08-Reuse.md#向上转型) - * [final关键字](book/08-Reuse.md#final关键字) - * [类初始化和加载](book/08-Reuse.md#类初始化和加载) - * [本章小结](book/08-Reuse.md#本章小结) -* [第九章 多态](book/09-Polymorphism.md) - * [向上转型回溯](book/09-Polymorphism.md#向上转型回溯) - * [深入理解](book/09-Polymorphism.md#深入理解) - * [构造器和多态](book/09-Polymorphism.md#构造器和多态) - * [返回类型协变](book/09-Polymorphism.md#返回类型协变) - * [使用继承设计](book/09-Polymorphism.md#使用继承设计) - * [本章小结](book/09-Polymorphism.md#本章小结) -* [第十章 接口](book/10-Interfaces.md) - * [抽象类和方法](book/10-Interfaces.md#抽象类和方法) - * [接口创建](book/10-Interfaces.md#接口创建) - * [抽象类和接口](book/10-Interfaces.md#抽象类和接口) - * [完全解耦](book/10-Interfaces.md#完全解耦) - * [多接口结合](book/10-Interfaces.md#多接口结合) - * [使用继承扩展接口](book/10-Interfaces.md#使用继承扩展接口) - * [接口适配](book/10-Interfaces.md#接口适配) - * [接口字段](book/10-Interfaces.md#接口字段) - * [接口嵌套](book/10-Interfaces.md#接口嵌套) - * [接口和工厂方法模式](book/10-Interfaces.md#接口和工厂方法模式) - * [本章小结](book/10-Interfaces.md#本章小结) -* [第十一章 内部类](book/11-Inner-Classes.md) - * [创建内部类](book/11-Inner-Classes.md#创建内部类) - * [链接外部类](book/11-Inner-Classes.md#链接外部类) - * [内部类this和new的使用](book/11-Inner-Classes.md#内部类this和new的使用) - * [内部类向上转型](book/11-Inner-Classes.md#内部类向上转型) - * [内部类方法和作用域](book/11-Inner-Classes.md#内部类方法和作用域) - * [匿名内部类](book/11-Inner-Classes.md#匿名内部类) - * [嵌套类](book/11-Inner-Classes.md#嵌套类) - * [内部类应用场景](book/11-Inner-Classes.md#内部类应用场景) - * [继承内部类](book/11-Inner-Classes.md#继承内部类) - * [重写内部类](book/11-Inner-Classes.md#重写内部类) - * [内部类局部变量](book/11-Inner-Classes.md#内部类局部变量) - * [内部类标识符](book/11-Inner-Classes.md#内部类标识符) - * [本章小结](book/11-Inner-Classes.md#本章小结) -* [第十二章 集合](book/12-Collections.md) - * [泛型和类型安全的集合](book/12-Collections.md#泛型和类型安全的集合) - * [基本概念](book/12-Collections.md#基本概念) - * [添加元素组](book/12-Collections.md#添加元素组) - * [集合的打印](book/12-Collections.md#集合的打印) - * [列表List](book/12-Collections.md#列表List) - * [迭代器Iterators](book/12-Collections.md#迭代器Iterators) - * [链表LinkedList](book/12-Collections.md#链表LinkedList) - * [堆栈Stack](book/12-Collections.md#堆栈Stack) - * [集合Set](book/12-Collections.md#集合Set) - * [映射Map](book/12-Collections.md#映射Map) - * [队列Queue](book/12-Collections.md#队列Queue) - * [集合与迭代器](book/12-Collections.md#集合与迭代器) - * [for-in和迭代器](book/12-Collections.md#for-in和迭代器) - * [本章小结](book/12-Collections.md#本章小结) -* [第十三章 函数式编程](book/13-Functional-Programming.md) - * [旧vs新](book/13-Functional-Programming.md#旧vs新) - * [Lambda表达式](book/13-Functional-Programming.md#Lambda表达式) - * [方法引用](book/13-Functional-Programming.md#方法引用) - * [函数式接口](book/13-Functional-Programming.md#函数式接口) - * [高阶函数](book/13-Functional-Programming.md#高阶函数) - * [闭包](book/13-Functional-Programming.md#闭包) - * [函数组合](book/13-Functional-Programming.md#函数组合) - * [Currying和Partial-Evaluation](book/13-Functional-Programming.md#Currying和Partial-Evaluation) - * [纯函数式编程](book/13-Functional-Programming.md#纯函数式编程) - * [本章小结](book/13-Functional-Programming.md#本章小结) -* [第十四章 流式编程](book/14-Streams.md) - * [流支持](book/14-Streams.md#流支持) - * [流创建](book/14-Streams.md#流创建) - * [中级流操作](book/14-Streams.md#中级流操作) - * [Optional类](book/14-Streams.md#Optional类) - * [终端操作](book/14-Streams.md#终端操作) - * [本章小结](book/14-Streams.md#本章小结) -* [第十五章 异常](book/15-Exceptions.md) - * [异常概念](book/15-Exceptions.md#异常概念) - * [基本异常](book/15-Exceptions.md#基本异常) - * [异常捕获](book/15-Exceptions.md#异常捕获) - * [自定义异常](book/15-Exceptions.md#自定义异常) - * [异常规范](book/15-Exceptions.md#异常规范) - * [任意异常捕获](book/15-Exceptions.md#任意异常捕获) - * [Java标准异常](book/15-Exceptions.md#Java标准异常) - * [finally关键字](book/15-Exceptions.md#finally关键字) - * [异常限制](book/15-Exceptions.md#异常限制) - * [异常构造](book/15-Exceptions.md#异常构造) - * [Try-With-Resources用法](book/15-Exceptions.md#Try-With-Resources用法) - * [异常匹配](book/15-Exceptions.md#异常匹配) - * [异常准则](book/15-Exceptions.md#异常准则) - * [异常指南](book/15-Exceptions.md#异常指南) - * [本章小结](book/15-Exceptions.md#本章小结) -* [第十六章 代码校验](book/16-Validating-Your-Code.md) - * [测试](book/16-Validating-Your-Code.md#测试) - * [前提条件](book/16-Validating-Your-Code.md#前提条件) - * [测试驱动开发](book/16-Validating-Your-Code.md#测试驱动开发) - * [日志](book/16-Validating-Your-Code.md#日志) - * [调试](book/16-Validating-Your-Code.md#调试) - * [基准测试](book/16-Validating-Your-Code.md#基准测试) - * [分析和优化](book/16-Validating-Your-Code.md#分析和优化) - * [风格检测](book/16-Validating-Your-Code.md#风格检测) - * [静态错误分析](book/16-Validating-Your-Code.md#静态错误分析) - * [代码重审](book/16-Validating-Your-Code.md#代码重审) - * [结对编程](book/16-Validating-Your-Code.md#结对编程) - * [重构](book/16-Validating-Your-Code.md#重构) - * [持续集成](book/16-Validating-Your-Code.md#持续集成) - * [本章小结](book/16-Validating-Your-Code.md#本章小结) -* [第十七章 文件](book/17-Files.md) - * [文件和目录路径](book/17-Files.md#文件和目录路径) - * [目录](book/17-Files.md#目录) - * [文件系统](book/17-Files.md#文件系统) - * [路径监听](book/17-Files.md#路径监听) - * [文件查找](book/17-Files.md#文件查找) - * [文件读写](book/17-Files.md#文件读写) - * [本章小结](book/17-Files.md#本章小结) -* [第十八章 字符串](book/18-Strings.md) - * [字符串的不可变](book/18-Strings.md#字符串的不可变) - * [重载和StringBuilder](book/18-Strings.md#重载和StringBuilder) - * [意外递归](book/18-Strings.md#意外递归) - * [字符串操作](book/18-Strings.md#字符串操作) - * [格式化输出](book/18-Strings.md#格式化输出) - * [常规表达式](book/18-Strings.md#常规表达式) - * [扫描输入](book/18-Strings.md#扫描输入) - * [StringTokenizer类](book/18-Strings.md#StringTokenizer类) - * [本章小结](book/18-Strings.md#本章小结) -* [第十九章 类型信息](book/19-Type-Information.md) - * [运行时类型信息](book/19-Type-Information.md#运行时类型信息) - * [类的对象](book/19-Type-Information.md#类的对象) - * [类型转换检测](book/19-Type-Information.md#类型转换检测) - * [注册工厂](book/19-Type-Information.md#注册工厂) - * [类的等价比较](book/19-Type-Information.md#类的等价比较) - * [反射运行时类信息](book/19-Type-Information.md#反射运行时类信息) - * [动态代理](book/19-Type-Information.md#动态代理) - * [Optional类](book/19-Type-Information.md#Optional类) - * [接口和类型](book/19-Type-Information.md#接口和类型) - * [本章小结](book/19-Type-Information.md#本章小结) -* [第二十章 泛型](book/20-Generics.md) - * [简单泛型](book/20-Generics.md#简单泛型) - * [泛型接口](book/20-Generics.md#泛型接口) - * [泛型方法](book/20-Generics.md#泛型方法) - * [复杂模型构建](book/20-Generics.md#复杂模型构建) - * [泛型擦除](book/20-Generics.md#泛型擦除) - * [补偿擦除](book/20-Generics.md#补偿擦除) - * [边界](book/20-Generics.md#边界) - * [通配符](book/20-Generics.md#通配符) - * [问题](book/20-Generics.md#问题) - * [自我约束类型](book/20-Generics.md#自我约束类型) - * [动态类型安全](book/20-Generics.md#动态类型安全) - * [泛型异常](book/20-Generics.md#泛型异常) - * [混入](book/20-Generics.md#混入) - * [潜在类型](book/20-Generics.md#潜在类型) - * [补偿不足](book/20-Generics.md#补偿不足) - * [辅助潜在类型](book/20-Generics.md#辅助潜在类型) - * [泛型的优劣](book/20-Generics.md#泛型的优劣) -* [第二十一章 数组](book/21-Arrays.md) - * [数组特性](book/21-Arrays.md#数组特性) - * [一等对象](book/21-Arrays.md#一等对象) - * [返回数组](book/21-Arrays.md#返回数组) - * [多维数组](book/21-Arrays.md#多维数组) - * [泛型数组](book/21-Arrays.md#泛型数组) - * [Arrays的fill方法](book/21-Arrays.md#Arrays的fill方法) - * [Arrays的setAll方法](book/21-Arrays.md#Arrays的setAll方法) - * [增量生成](book/21-Arrays.md#增量生成) - * [随机生成](book/21-Arrays.md#随机生成) - * [泛型和基本数组](book/21-Arrays.md#泛型和基本数组) - * [数组元素修改](book/21-Arrays.md#数组元素修改) - * [数组并行](book/21-Arrays.md#数组并行) - * [Arrays工具类](book/21-Arrays.md#Arrays工具类) - * [数组拷贝](book/21-Arrays.md#数组拷贝) - * [数组比较](book/21-Arrays.md#数组比较) - * [流和数组](book/21-Arrays.md#流和数组) - * [数组排序](book/21-Arrays.md#数组排序) - * [binarySearch二分查找](book/21-Arrays.md#binarySearch二分查找) - * [parallelPrefix并行前缀](book/21-Arrays.md#parallelPrefix并行前缀) - * [本章小结](book/21-Arrays.md#本章小结) -* [第二十二章 枚举](book/22-Enumerations.md) - * [基本功能](book/22-Enumerations.md#基本功能) - * [方法添加](book/22-Enumerations.md#方法添加) - * [switch语句](book/22-Enumerations.md#switch语句) - * [values方法](book/22-Enumerations.md#values方法) - * [实现而非继承](book/22-Enumerations.md#实现而非继承) - * [随机选择](book/22-Enumerations.md#随机选择) - * [使用接口组织](book/22-Enumerations.md#使用接口组织) - * [使用EnumSet替代Flags](book/22-Enumerations.md#使用EnumSet替代Flags) - * [使用EnumMap](book/22-Enumerations.md#使用EnumMap) - * [常量特定方法](book/22-Enumerations.md#常量特定方法) - * [多次调度](book/22-Enumerations.md#多次调度) - * [本章小结](book/22-Enumerations.md#本章小结) -* [第二十三章 注解](book/23-Annotations.md) - * [基本语法](book/23-Annotations.md#基本语法) - * [编写注解处理器](book/23-Annotations.md#编写注解处理器) - * [使用javac处理注解](book/23-Annotations.md#使用javac处理注解) - * [基于注解的单元测试](book/23-Annotations.md#基于注解的单元测试) - * [本章小结](book/23-Annotations.md#本章小结) -* [第二十四章 并发编程](book/24-Concurrent-Programming.md) - * [术语问题](book/24-Concurrent-Programming.md#术语问题) - * [并发的超能力](book/24-Concurrent-Programming.md#并发的超能力) - * [针对速度](book/24-Concurrent-Programming.md#针对速度) - * [四句格言](book/24-Concurrent-Programming.md#四句格言) - * [残酷的真相](book/24-Concurrent-Programming.md#残酷的真相) - * [本章其余部分](book/24-Concurrent-Programming.md#本章其余部分) - * [并行流](book/24-Concurrent-Programming.md#并行流) - * [创建和运行任务](book/24-Concurrent-Programming.md#创建和运行任务) - * [终止耗时任务](book/24-Concurrent-Programming.md#终止耗时任务) - * [CompletableFuture类](book/24-Concurrent-Programming.md#CompletableFuture类) - * [死锁](book/24-Concurrent-Programming.md#死锁) - * [构造函数非线程安全](book/24-Concurrent-Programming.md#构造函数非线程安全) - * [复杂性和代价](book/24-Concurrent-Programming.md#复杂性和代价) - * [本章小结](book/24-Concurrent-Programming.md#本章小结) -* [第二十五章 设计模式](book/25-Patterns.md) - * [概念](book/25-Patterns.md#概念) - * [构建型](book/25-Patterns.md#构建型) - * [面向实施](book/25-Patterns.md#面向实施) - * [工厂模式](book/25-Patterns.md#工厂模式) - * [函数对象](book/25-Patterns.md#函数对象) - * [接口改变](book/25-Patterns.md#接口改变) - * [解释器](book/25-Patterns.md#解释器) - * [回调](book/25-Patterns.md#回调) - * [多次调度](book/25-Patterns.md#多次调度) - * [模式重构](book/25-Patterns.md#模式重构) - * [抽象用法](book/25-Patterns.md#抽象用法) - * [多次派遣](book/25-Patterns.md#多次派遣) - * [访问者模式](book/25-Patterns.md#访问者模式) - * [RTTI的优劣](book/25-Patterns.md#RTTI的优劣) - * [本章小结](book/25-Patterns.md#本章小结) - -### 附录 - -* [附录:补充](book/Appendix-Supplements.md) - * [可下载的补充](book/Appendix-Supplements.md#可下载的补充) - * [通过Thinking-in-C来巩固Java基础](book/Appendix-Supplements.md#通过Thinking-in-C来巩固Java基础) - * [动手实践](book/Appendix-Supplements.md#动手实践) -* [附录:编程指南](book/Appendix-Programming-Guidelines.md) - * [设计](book/Appendix-Programming-Guidelines.md#设计) - * [实现](book/Appendix-Programming-Guidelines.md#实现) -* [附录:文档注释](book/Appendix-Javadoc.md) -* [附录:对象传递和返回](book/Appendix-Passing-and-Returning-Objects.md) - * [传递引用](book/Appendix-Passing-and-Returning-Objects.md#传递引用) - * [本地拷贝](book/Appendix-Passing-and-Returning-Objects.md#本地拷贝) - * [控制克隆](book/Appendix-Passing-and-Returning-Objects.md#控制克隆) - * [不可变类](book/Appendix-Passing-and-Returning-Objects.md#不可变类) - * [本章小结](book/Appendix-Passing-and-Returning-Objects.md#本章小结) -* [附录:流式IO](book/Appendix-IO-Streams.md) - * [输入流类型](book/Appendix-IO-Streams.md#输入流类型) - * [输出流类型](book/Appendix-IO-Streams.md#输出流类型) - * [添加属性和有用的接口](book/Appendix-IO-Streams.md#添加属性和有用的接口) - * [Reader和Writer](book/Appendix-IO-Streams.md#Reader和Writer) - * [RandomAccessFile类](book/Appendix-IO-Streams.md#RandomAccessFile类) - * [IO流典型用途](book/Appendix-IO-Streams.md#IO流典型用途) - * [本章小结](book/Appendix-IO-Streams.md#本章小结) -* [附录:标准IO](book/Appendix-Standard-IO.md) - * [执行控制](book/Appendix-Standard-IO.md#执行控制) -* [附录:新IO](book/Appendix-New-IO.md) - * [ByteBuffer](book/Appendix-New-IO.md#ByteBuffer) - * [转换数据](book/Appendix-New-IO.md#转换数据) - * [获取原始类型](book/Appendix-New-IO.md#获取原始类型) - * [视图缓冲区](book/Appendix-New-IO.md#视图缓冲区) - * [使用缓冲区进行数据操作](book/Appendix-New-IO.md#使用缓冲区进行数据操作) - * [内存映射文件](book/Appendix-New-IO.md#内存映射文件) - * [文件锁定](book/Appendix-New-IO.md#文件锁定) -* [附录:理解equals和hashCode方法](book/Appendix-Understanding-equals-and-hashCode.md) - * [equals典范](book/Appendix-Understanding-equals-and-hashCode.md#equals典范) - * [哈希和哈希码](book/Appendix-Understanding-equals-and-hashCode.md#哈希和哈希码) - * [调整HashMap](book/Appendix-Understanding-equals-and-hashCode.md#调整HashMap) -* [附录:集合主题](book/Appendix-Collection-Topics.md) - * [示例数据](book/Appendix-Collection-Topics.md#示例数据) - * [List表现](book/Appendix-Collection-Topics.md#List表现) - * [Set表现](book/Appendix-Collection-Topics.md#Set表现) - * [在Map中使用函数式操作](book/Appendix-Collection-Topics.md#在Map中使用函数式操作) - * [选择Map的部分](book/Appendix-Collection-Topics.md#选择Map的部分) - * [集合的fill方法](book/Appendix-Collection-Topics.md#集合的fill方法) - * [使用Flyweight自定义集合和Map](book/Appendix-Collection-Topics.md#使用Flyweight自定义集合和Map) - * [集合功能](book/Appendix-Collection-Topics.md#集合功能) - * [可选操作](book/Appendix-Collection-Topics.md#可选操作) - * [Set和存储顺序](book/Appendix-Collection-Topics.md#Set和存储顺序) - * [队列](book/Appendix-Collection-Topics.md#队列) - * [理解Map](book/Appendix-Collection-Topics.md#理解Map) - * [集合工具类](book/Appendix-Collection-Topics.md#集合工具类) - * [持有引用](book/Appendix-Collection-Topics.md#持有引用) - * [避免旧式类库](book/Appendix-Collection-Topics.md#避免旧式类库) - * [本章小结](book/Appendix-Collection-Topics.md#本章小结) -* [附录:并发底层原理](book/Appendix-Low-Level-Concurrency.md) - * [线程](book/Appendix-Low-Level-Concurrency.md#线程) - * [异常捕获](book/Appendix-Low-Level-Concurrency.md#异常捕获) - * [资源共享](book/Appendix-Low-Level-Concurrency.md#资源共享) - * [volatile关键字](book/Appendix-Low-Level-Concurrency.md#volatile关键字) - * [原子性](book/Appendix-Low-Level-Concurrency.md#原子性) - * [关键部分](book/Appendix-Low-Level-Concurrency.md#关键部分) - * [库组件](book/Appendix-Low-Level-Concurrency.md#库组件) - * [本章小结](book/Appendix-Low-Level-Concurrency.md#本章小结) -* [附录:数据压缩](book/Appendix-Data-Compression.md) - * [使用Gzip简单压缩](book/Appendix-Data-Compression.md#使用Gzip简单压缩) - * [使用zip多文件存储](book/Appendix-Data-Compression.md#使用zip多文件存储) - * [Java的jar](book/Appendix-Data-Compression.md#Java的jar) -* [附录:对象序列化](book/Appendix-Object-Serialization.md) - * [查找类](book/Appendix-Object-Serialization.md#查找类) - * [控制序列化](book/Appendix-Object-Serialization.md#控制序列化) - * [使用持久化](book/Appendix-Object-Serialization.md#使用持久化) -* [附录:静态语言类型检查](book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md) - * [前言](book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md#前言) - * [静态类型检查和测试](book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md#静态类型检查和测试) - * [如何提升打字](book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md#如何提升打字) - * [生产力的成本](book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md#生产力的成本) - * [静态和动态](book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md#静态和动态) -* [附录:C++和Java的优良传统](book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md) -* [附录:成为一名程序员](book/Appendix-Becoming-a-Programmer.md) - * [如何开始](book/Appendix-Becoming-a-Programmer.md#如何开始) - * [码农生涯](book/Appendix-Becoming-a-Programmer.md#码农生涯) - * [百分之五的神话](book/Appendix-Becoming-a-Programmer.md#百分之五的神话) - * [重在动手](book/Appendix-Becoming-a-Programmer.md#重在动手) - * [像打字般编程](book/Appendix-Becoming-a-Programmer.md#像打字般编程) - * [做你喜欢的事](book/Appendix-Becoming-a-Programmer.md#做你喜欢的事) -* [词汇表](GLOSSARY.md) - diff --git a/assets/LogoMark.png b/assets/LogoMark.png new file mode 100644 index 00000000..8fdee22a Binary files /dev/null and b/assets/LogoMark.png differ diff --git a/assets/QQGroupQRCode.jpg b/assets/QQGroupQRCode.jpg new file mode 100644 index 00000000..f74ee604 Binary files /dev/null and b/assets/QQGroupQRCode.jpg differ diff --git a/book.json b/book.json deleted file mode 100644 index f99031f5..00000000 --- a/book.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "title": "《On Java 8》中文版", - "author": "LingCoder", - "description": "根据Bruce Eckel大神的新书On Java 8翻译,可以说是事实上的Thinking in Java 5th", - "language": "zh-hans", - "gitbook": "3.2.3", - "styles": { - "website": "styles/website.css", - "ebook": "styles/ebook.css", - "pdf": "styles/pdf.css", - "mobi": "styles/mobi.css", - "epub": "styles/epub.css" - }, - "structure": { - "readme": "Introduction.md" - }, - "plugins": [ - "splitter", - "edit-link", - "search-pro", - "emphasize", - "toggle-chapters", - "katex", - "mermaid-gb3", - "advanced-emoji", - "include-codeblock" - ], - "pluginsConfig": { - "edit-link": { - "base": "https://github.com/lingcoder/OnJava8/edit/master", - "label": "Edit This Page" - } - } -} diff --git a/book/00-Introduction.md b/book/00-Introduction.md deleted file mode 100644 index 2b793d95..00000000 --- a/book/00-Introduction.md +++ /dev/null @@ -1,101 +0,0 @@ - -# 简介 - -> "我的语言极限,即是我的世界的极限。" ——路德维希·维特根斯坦(*Wittgenstein*) - -这句话对于编程语言来说也一样。你所使用的编程语言会将你的思维模式固化并逐渐远离其他语言。Java 作为一门傲娇的派生语言尤其如此。早期语言设计者为了不想在项目中使用 C++ 而创造了这种看起来很像 C++,却比 C++ 有了改进的新语言。其最核心的变化就是包含虚拟机和垃圾回收机制。这两个概念在本书之后的章节会有详细描述。 此外,Java 还在其他方面推动行业了发展。例如,现在绝大多数编程语言都包含文档注释语法和 HTML 文档生成的工具。Java 最主要的概念之一"对象"来自 SmallTalk 语言。Java 定义了"对象"(在下一章中描述)为编程的基本单元。于是,万物皆对象。 - -时间已经检验了这种信念,并发现它太过狂热。有些人甚至认为"对象"是完全错误的概念,应该被丢弃。就我个人而言,把一切都当成一个对象不仅是一个不必要的负担,而且还会招致许多设计朝着不好的方向发展。尽管如此,"对象"的概念依然有其闪光点。固执地要求所有东西都是一个对象(特别是一直到最底层级别)是一个设计错误。相反,完全逃避"对象"的概念似乎同样太过苛刻。 - -其他 Java 语言决策并没有像承诺的那样完成。在本书中,我会尝试解释这些。你不仅了解这些功能,还要了解为什么他们可能对你不太适用。这无关 Java 是一种好语言或者坏语言。一旦你了解了该语言的缺陷和局限性,你就能够: - -1. 遇到"已停用"的功能特性时不会疑惑。 - -2. 熟悉语言边界,更好地设计和编码。 - -编程有关管理复杂性;问题的复杂性依赖于机器的复杂性。由于这种复杂性,我们的大多数编程项目都失败了。许多语言设计决策时都考虑到了复杂性,但在某些时候,其他问题也势必不会少的,程序员不可避免地"碰壁"。例如,C++ 必须向后兼容 C(允许 C 程序员轻松迁移),并且效率很高。这些目标诚然很好,并且也解释了为什么 C++ 在编程界取得了成功。为了保证兼容性的代价会造成语言额外的复杂性。当然,你可以责怪程序员和管理人员,但如果一种语言可以通过捕捉异常来提供有用的信息,为什么不呢? - -虽然 Visual BASIC(VB)绑定在 BASIC 上,但 BASIC 实际上并不是一种可扩展的语言。在 VB 上堆积的所有扩展造成大量无法维护的语法。Perl 向后兼容 awk,sed,grep 以及其它要替换的 Unix 工具,因此它经常被指责生成"只写代码"(也就是说,你看不懂自己的代码)。另一方面,C ++,VB,Perl 和其他语言(如 SmallTalk)的一些设计工作集中在复杂性问题上,因此在解决某些类型的问题方面非常成功。通信革命使我们所有人更容易地相互沟通:一对一,团体和行星。据说下一次革命会形成一种新全球性的思想,这种思想源于足量的人之间的相互联系。Java 可能会也可能不会成为这场革命的工具之一,但至少这种可能性让我觉得我现在正在做的传道授业是一件有意义的事情。 - - - - -## 前提条件 - -阅读本书需要读者对编程有基本的了解: - -程序是由一系列代码块,子句或函数组成的控制语句的集合。比如 "if","while"判断语句等等。 - -你可能已经在学校、网络或其他书本上了熟悉了这样。只要你对自己的编程基础有自信,你就可以完成本书的学习。 - -你可以通过在 On Java 8的网站上免费下载 Think in C 来补充学习 Java 所需要的前置知识。 本书介绍了 Java 语言的基本控制机制以及面对对象编程的概念。在本书中我引述了一些 C/C ++ 语言中的一些特性来帮助读者更好的理解 Java。 毕竟 Java 是在它们的基础之上发明的。理解他们之间的区别,有助于读者更好的学习 Java。 - - -## JDK文档 - -甲骨文公司已经提供了标准免费的 JDK 文档。除非必要,否则在本书中将不再赘述有关 API 使用的细节。使用浏览器来即时搜索最新最全的 JDK 文档要好过翻阅本书来查找。只有在需要补充特定的示例时,我才会提供有关的额外描述。 - - -## C编程思想 - -*Thinking in C* 已经可以在 www.OnJava8.com 免费下载。Java 的基础语法是基于 C 语言的。*Thinking in C* 中有更适合初学者的编程基础介绍。 我已经委托 Chuck Allison 将这本 C 基础的书籍作为独立产品附赠于本书的 CD 中。希望大家在阅读本书时,都已具备了学习 JAVA 的良好基础。 - - -## 源码下载 - -本书中所有源代码的示例都在版权保护的前提下通过 GITHUB 免费提供。你可以将这些代码用于教育。任何人不得在未经正确引用代码来源的情况下随意重新发布此代码示例。在每个代码文件中,你都可以找到以下版权声明文件作为参考: - -**Copyright.txt** - -©2017 MindView LLC。版权所有。如果上述版权声明,本段和以下内容,特此授予免费使用,复制,修改和分发此计算机源代码(源代码)及其文档的许可,且无需出于下述目的的书面协议所有副本中都有五个编号的段落。 - -1. 允许编译源代码并将编译代码仅以可执行格式包含在个人和商业软件程序中。 - -2. 允许在课堂情况下使用源代码而不修改源代码,包括在演示材料中,前提是"On Java 8"一书被引用为原点。 - -3. 可以通过以下方式获得将源代码合并到印刷媒体中的许可:MindView LLC,PO Box 969,Crested Butte,CO 81224 MindViewInc@gmail.com - -4. 源代码和文档的版权归 MindView LLC 所有。提供的源代码没有任何明示或暗示的担保,包括任何适销性,适用于特定用途或不侵权的默示担保。MindView LLC 不保证任何包含源代码的程序的运行不会中断或没有错误。MindView LLC 不对任何目的的源代码或包含源代码的任何软件的适用性做出任何陈述。包含源代码的任何程序的质量和性能的全部风险来自源代码的用户。用户理解源代码是为研究和教学目的而开发的,建议不要仅仅因任何原因依赖源代码或任何包含源代码的程序。如果源代码或任何产生的软件证明有缺陷,则用户承担所有必要的维修,修理或更正的费用。 - -5. 在任何情况下,MINDVIEW LLC 或其出版商均不对任何一方根据任何法律理论对直接,间接,特殊,偶发或后果性损害承担任何责任,包括利润损失,业务中断,商业信息丢失或任何其他保险公司。由于 MINDVIEW LLC 或其出版商已被告知此类损害的可能性,因此使用本源代码及其文档或因无法使用任何结果程序而导致的个人受伤或者个人受伤。MINDVIEW LLC 特别声明不提供任何担保,包括但不限于对适销性和特定用途适用性的暗示担保。此处提供的源代码和文档基于"原样"基础,没有MINDVIEW LLC的任何随附服务,MINDVIEW LLC 没有义务提供维护,支持,更新,增强或修改。 - - -**请注意**,MindView LLC 仅提供以下唯一网址发布更新书中的代码示例,https://github.com/BruceEckel/OnJava8-examples 。你可在上述条款范围内将示例免费使用于项目和课堂中。 - -如果你在源代码中发现错误,请在下面的网址提交更正:https://github.com/BruceEckel/OnJava8-examples/issues - - -## 编码样式 - -本书中代码标识符(关键字,方法,变量和类名)以粗体,固定宽度代码字体显示。像"**class**"这种在代码中高频率出现的关键字可能让你觉得粗体有点乏味。其他显示为正常字体。本书文本格式尽可能遵循 Oracle 常见样式,并保证在大多数 Java 开发环境中被支持。书中我使用了自己喜欢的字体风格。Java 是一种自由的编程语言,你也可以使用 IDE(集成开发环境)工具(如 IntelliJ IDEA,Eclipse 或 NetBeans)将格式更改为适合你的格式。 - -本书代码文件使用自动化工具进行测试,并在最新版本的 Java 编译通过(除了那些特别标记的错误之外)。本书重点介绍并使用 Java 8 进行测试。如果你必须了解更早的语言版本,可以在 [www.OnJava8.com](http://www.OnJava8.com) 免费下载 *Thinking in Java*。 - - -## BUG提交 - -本书经过多重校订,但还是难免有所遗漏被新读者发现。如果你在正文或示例中发现任何错误的内容,请在[此处](https://github.com/BruceEckel/OnJava8-examples/issues)提交错误以及建议更正,作者感激不尽。 - - -## 邮箱订阅 - -你可以在 [www.OnJava8.com上](http://www.OnJava8.com) 订阅邮件。邮件不含广告并尽量提供干货。 - - -## Java图形界面 - -Java 在图形用户界面和桌面程序方面的发展可以说是一段悲伤的历史。Java 1.0 中图形用户界面(GUI)库的原始设计目标是让用户能在所有平台提供一个漂亮的界面。但遗憾的是这个理想没有实现。相反,Java 1.0 抽象窗口工具包(AWT)在所有平台都表现平平,并且有诸多限制;你只能使用四种字体。另外, Java 1.0 AWT 编程模型也很笨拙且不面向对象。我的一个曾在 Java 设计期间工作过的学生道出了缘由:早期的 AWT 设计是在仅仅在一个月内构思,设计和实施的。不得不说这是一个"奇迹",但同时更是"设计失败"的绝佳教材。 - -在 Java 1.1 版本的AWT中 情况有所改善,事件模型采用了更加清晰的面向对象方法,并添加了JavaBeans,致力于面向易于创建可视化编程环境的组件编程模型(已废弃)。 - -Java 2(Java 1.2)通过基本上用 Java 基类(JFC)替换所有内容来完成从旧 Java 1.0 AWT的转换,其中 GUI 部分称为 "Swing"。这是一组丰富的JavaBeans,它们创建了一个合理的GUI。修订版 3(3之前都不好)比以往更适用于开发图形界面程序。 - -Sun 在图形界面的最后一次尝试,称为 JavaFX。当 Oracle 收购 Sun 时,他们将原来雄心勃勃的项目(包括脚本语言)改为库,现在它似乎是 Java 官方唯一还在开发中的UI工具包(参见维基百科关于JavaFX 的文章) - 但即使如此。JavaFX 似乎最终后也失败了。 - -现今 Swing 依然是 Java 发行版的一部分(只接受维护,不再有新功能开发),而 Java 现在是一个开源项目,它应该始终可用。此外,Swing 和 JavaFX 有一些有限的交互性。这些可能是为了帮助开发者过渡到 JavaFX。 - -桌面程序领域似乎从未尝勾起 Java 设计师的野心。Java 没有在图形界面取得该有的一席之地。另外,曾被大肆吹嘘的 JavaBeans 也没有获得任何影响力。(许多不幸的作者花了很多精力在 Swing 上编写书籍,甚至只用 JavaBeans 编写书籍)。Java 图形界面程序大多数情况下仅用于 集成开发环境(IDE)和一些企业内部应用程序。你是可以使用 Java 开发图形界面,但这不是 Java 最擅长的领域。如果你必须学习 Swing,它可以在 *Thinking in Java* 第4版(可从 www.OnJava8.com 获得)和其他专门的书籍中学习。。 - - - - diff --git a/book/00-On-Java-8.md b/book/00-On-Java-8.md deleted file mode 100644 index 6ef35c28..00000000 --- a/book/00-On-Java-8.md +++ /dev/null @@ -1,63 +0,0 @@ -
- cover -
- - - -
-

On Java 8

-
- -
-

Bruce Eckel

- -
- -
MindView LLC
- - -
2017
- - -
©MindView LLC 版权所有
- - - - - - - - - - -# On Java 8 - - - -**版权©2017** - - -**作者 Bruce Eckel, President, MindView LLC.** - - -**版本号:7** - - -**ISBN 978-0-9818725-2-0** - - -**原书可在该网站购买 [www.OnJava8.com](http://www.OnJava8.com)** - - - - -本书出版自美国,版权所有,翻版必究。未经授权不得非法存储在检索系统中,或以电子,机械,影印,录制任何形式传输等。制造商和销售商使用商标用来区分其产品标识。如果这些名称出现在这本书中,并且出版商知道商标要求,则这些名称已经用大写字母或所有大写字母打印。 - -Java 是甲骨文公司(Oracle. Inc.)的商标。Windows 95,Windows NT,Windows 2000,Windows XP,Windows 7,Windows 8和 Windows 10是微软公司(Microsoft Corporation)的商标。 -此处提及的所有其他产品名称和公司名称均为其各自所有者的财产。作者和出版商在编写本书时已经仔细校对过,但不作任何明示或暗示的保证,对错误或遗漏不承担任何责任。对于因使用此处包含的信息或程序而产生的偶然或间接损失,我们不承担任何责任。 - -这本书是以平板电脑和计算机为载体的电子书,非传统纸质版书籍。 -故所有布局和格式设计旨在优化您在各种电子书阅读平台和系统上的观看体验。 -封面由 Daniel Will-Harris 设计,[www.Will-Harris.com](http://www.Will-Harris.com)。 - - \ No newline at end of file diff --git a/book/00-Preface.md b/book/00-Preface.md deleted file mode 100644 index 30486a6c..00000000 --- a/book/00-Preface.md +++ /dev/null @@ -1,126 +0,0 @@ - - -# 前言 - -> 本书基于 Java 8版本来教授目前最惯用的Java编码形式。 - -在此之前,我的另一本 Java 书籍 Thinking in Java 第4版(《Java编程思想》 Prentice Hall 2006)对于 Java 5的编程依然有指导意义。Java 5是用于 Android 编程的语言版本。 - -随着 Java 8的出现,这门语言在许多地方发生了翻天覆地的变化。新的 Java 代码在使用和实现上与以往不尽相同。这也是为什么时隔两年后我创作了这本新书。《On Java 8》旨在面向已具有编程基础的开发者们。对于初学者,可以先在 [Code.org](http://Code.org) 或者 [Khan Academy](https://www.khanacademy.org/computing/computer-programming) 等网站上补充必要的前置知识。同时,[OnJava8.com](http://www.OnJava8.com) 上也有免费的 Thinking in C(《C编程思想》)专题知识。 - -与几年前我们依赖印刷媒体相比,像 YouTube,博客和 StackOverflow 这样的网站让寻找答案变得非常容易。请将这些与坚持不懈的努力相结合。你可以将本书作为你的编程入门书籍。同样她也适用于想要扩展知识的在职程序员。每次在世界各地的演讲中,我都非常感谢 Thinking in Java 这本书给我带来的所有荣誉。事实证明,这些荣誉对我现在的 [Reinventing Business](http://www.reinventing-business.com) 项目中和加强外界与公司的联系是非常宝贵的。最后,写这本书的原因之一是支持我 [Reinventing Business](http://www.reinventing-business.com) 重塑,似乎下一个合乎逻辑的步骤是实际创建一个所谓的蓝绿色组织(Teal Organization)。我希望这本书可以成为该项目的一种众筹。 - - -## 教学目标 - -每章教授一个或一组相关的概念,并且这些知识不依赖于尚未学习到的章节。这样以来,学习者可以在当前知识的背景框架下循序渐进地掌握 JAVA 。 - -本书的教学目标: - - -1. 循序渐进地呈现学习内容,以便于你在不依赖后置知识框架的情况下轻松完成现有的学习任务,同时尽量保证前面章节的内容在后面的学习中得到运用。如果确有必要引入我们还没学习到的知识概念,我会做个简短地介绍。 - -2. 尽可能地使用简单和简短的示例,方便读者理解。而不强求引入解决实际问题的例子。因为我发现,相比解决某个实际问题,读者更乐于看到自己真正理解了示例的每个细节。或许我会因为这些"玩具示例"而被一些人所诟病,但我更愿意看到我的读者们因此能保持饶有兴趣地学习。 - -3. 把我知道的以及我认为对于你学习语言很重要的东西都告诉你。我认为信息的重要性是分层次结构的。绝大多数情况下,我们没必要弄清问题的所有本质。好比编程语言中的某些特性和实现细节,95%的程序员都不需要取知道。这些细节除了会加重你的学习成本,还让你更觉得这门语言好复杂。如果你非要考虑这些细节,那么它还会迷惑该代码的阅读者/维护者,所以我主张选择简单的方法解决问题。 - -4. 希望本书能为你打下坚实的基础,方便你将来学习更难的课程和书籍。 - - - -## 语言设计错误 - -每种语言都有设计错误。当新程序员必须涉足语言特性,并猜测应用场景和使用方式时,他们体验到极大的不确定性和挫折感。承认错误令人尴尬,但这种糟糕的初学者经历比认识到你错了什么还要糟糕。哎,每一种设语言/库的设计错误都会永久地嵌入在 Java 的发行版中。 - -诺贝尔经济学奖得主约瑟夫·斯蒂格利茨(Joseph Stiglitz)有一套适用于这里的人生哲学,叫做"承诺升级理论":继续犯错误的成本由别人承担,而承认错误的成本由自己承担。 - -如果你有读过我过去的作品,那么你应该知道,我一般倾向于指出这些语言设计错误。Java 已经发展出了一批狂热的追随者,他们对待这门语言更像是阵营而不是编程工具。因为我已经写过有关 Java 的书,所以他们理所当然的认为我也是这个"阵营"的一份子。于是,当我指出Java的错误时,就会造成两种影响: - -1. 最初,会有许多错误"阵营"的人成为了牺牲品。最终,时隔多年,大家都意识到这是个设计上的错误,然后错误就这样成为了 Java 历史的一部分。 - -2. 更重要的是,新程序员并没有经历过"他们"为什么要采用这种实现方式的斗争过程。特别是那些隐隐约约感觉不对却依然说服自己"我必须要这么做"或者"我只是没学明白"从而继续错下去的人。更糟糕的是,教授这些编程知识的老师们没能深入的去研究这里是否有设计上的错误,而是继续错误的解读。通过了解语言设计上的错误,能让开发者们更好的理解和意识到错误的本质,从而更快地进步。 - -理解编程语言的设计错误至关重要,甚至影响程序员的开发效率。一些公司在开发过程中会避免使用语言的某些功能特性。表面上看这些"功能特性"很高大上,但是弄不好却可能出现意料之外的错误,影响整个开发进程。 - -已知的语言设计错误也会给新的一门编程语言的发明提供参考意义。探索一门语言能做什么是很有趣的一件事,而语言设计错误能提醒你哪些"坑"是不能再趟的。多年以来,我一直感觉 Java 的设计者们有点脱离群众的。Java 有些设计错误不免错的太明显,我都怀疑语言设计师们到底是为了用户服务还是出于其他的动机设计了这些功能。Java 语言有许多臭名昭著的设计错误,很可能这也是诱惑所在。我感觉这是对程序员的不尊重。为此我很长时间不想于 Java 有任何瓜葛,很大程度上,这也是我为什么不想碰 Java的原因吧。 - -现今当我再来看 Java 8时,我发现与之前有许多不同的地方。Java 语言的设计师们似乎对于语言和用户的态度发生了根本性上的改变。在多年忽视用户投诉之后,许多功能和库已经被语言搞砸了。 - -新功能的设计与以往有很大不同。掌舵者开始重视程序员的编程经验。新功能最终都在努力使语言变得更好,而不仅仅是停留在快速添加想法而不深入研究它们的含义。有一些新功能实现上非常优雅(至少在 Java 约束下尽可能优雅)。 - -我猜测可能是一些人离开设计组让他们意识到了这点。我没想到会有这些变化!因为这些原因吧,写这本书的体验要比以往的经历要好得多。Java 8包含了一系列基础和重要的改进。哎,不过 Java有严格的"向后兼容"承诺。所以可能我们不大可能看到戏剧性的变化,当然我希望我是错的。尽管如此,我很赞赏那些敢于自我颠覆,并为 Java设定更好路线的人。第一次,对于自己所写的部分 Java 8代码我终于可以说"我喜欢这个!" - -最后,本书所著时间似乎也很不错,因为 Java 8引入的新功能已经强烈的影响了今后Java的编码方式。截止我在写这本书时,Java 9似乎更专注于对语言底层的基础结构功能的重要更新,但是这些并不会影响本书所关注的编码类型。话说回来,得益于电子书出版形式的便捷,如果我发现本书有需要更新或添加的内容,我可以很快将新版本推送给现有读者。 - - - -## 测试用例 - -本书中的代码示例基于 Java 8和 Gradle 编译构建,并且这些代码示例都保存在[这个自由访问的GitHub的仓库](https://github.com/BruceEckel/OnJava8-Examples) 中。我们需要内置的测试框架在每次构建系统时运行,否则你将无法保证自己代码的可靠性。为了在本书中实现这一点,我创建了一个测试系统来显示和验证大多数示例的输出结果。这个输出结果我会附加在示例结尾的代码块中。有时仅显示必要的那几行或者首尾行。利用这种方式来改善读者的阅读和学习体验,同时也提供了一种验证示例正确性的方法。 - - - -## 普及性 - -Java 的普及性对于其受欢迎程度有重要意义。学习 Java 会让你更容易找到工作。相关的培训材料,课程和其他可用的学习资源也很多。对于企业来说,招聘 Java 程序员也相对容易。如果你不喜欢 Java 语言,那么最好不要拿他当作你谋生的工具,因为这种生活体验并不好。作为一家公司,在技术选型前一定不要单单只考虑 Java 程序员好招。每种语言都有其适用的范围,有可能你们的业务更适用于另一种编程语言来达到事半功倍的效果。如果你真的喜欢Java,那么欢迎你。希望这本书能丰富你的编程经验! - - - -## 关于安卓 - -这本书基于 Java 8版本。如果你是 Andriod 程序员,请务必学习 Java 5。在《On Java 8》出版的时候,我的另一本基于 Java 5的著作 Thinking in Java 4th Edition(《Java编程思想》第四版)已经可以在[www.OnJava8.com](http://www.OnJava8.com)上免费下载了。此外,还有许多其他专用于 Andriod 编程的资源。 - - - - -## 电子版权声明 - -《On Java 8》仅提供电子版,并且仅通过 [www.OnJava8.com](http://www.OnJava8.com) 提供。任何未经 授权的其他来源或流传送机构都是非法的。本作品受版权保护!未经许可,请勿通过以任何方式分享或发布。您可以使用这些示例进行教学,只要不对本书非法重新出版。有关完整详细信息,请参阅示例分发中的 Copyright.txt 文件。对于视觉障碍者,电子版本有可搜索性,字体大小调整或文本到语音等诸多好处。 - -任何购买这本书的读者,还需要一台计算机来运行和写作代码。另外电子版在计算机上和移动设备上的显示效果俱佳,推荐使用平板设备阅读。相比购买传统纸质版的价格,平板电脑价格都足够便宜。在床上阅读电子版比看这样一本厚厚的实体书要方便得多。起初你可能会有些不习惯,但我相信很快你就会发现它带来的优点远胜过不适。我已经走过这个阶段,Google Play 图书的浏览器阅读体验非常好,包括在 Linux 和 iOS 设备上。作为一次尝试,我决定尝试通过 Google 图书进行出版。 - -**注意**:在撰写本文时,通过 Google Play 图书网络浏览器应用阅读图书虽然可以忍受,但体验还是有点差强人意,我强烈推荐读者们使用平板电脑来阅读。 - - - - -## 版本说明 - -本书采用 Pandoc 风格的 Markdown 编写,使用 Pandoc 生成 ePub v3格式。 - -正文字体为 Georgia,标题字体为 Verdana。 代码字体使用的 Ubuntu Mono,因为它特别紧凑,单行能容纳更多的代码。 我选择将代码内联(而不是将列表放入图像,因为我看过一些书籍),因为对我来说让读者能够在调整正文字体大小时,代码块也可自适应调整的功能非常重要(否则,买电子版,还图什么呢?)。 - -本书的提取,编译和测试代码示例的构建过程都是自动化的。所有自动化操作都是通过我在 Python 3中编写的程序来实现的。 - - - - -## 封面设计 - -《On Java 8》的封面通过 W.P.A.(Works Progress Administration 1935年至1943年美国大萧条期间的一个巨大项目,它使数百万失业人员重新就业)的马赛克创作的(WPA,这是)。它还让我想起了绿野仙踪(The Wizard of Oz)系列丛书中的插图。 我的好朋友、设计师丹尼尔威尔哈里斯([www.will-harris.com](http://www.will-harris.com))和我都喜欢这个形象。 - - - - -## 感谢的人 - -感谢 Domain-Driven Design(《领域驱动设计》 )的作者 Eric Evans 建议书名,以及其他新闻组校对帮助。 - -感谢 James Ward 为我开始使用 Gradle 工具构建这本书,以及他多年来的帮助和友谊。 - -感谢 Ben Muschko 构建文件的抛光工作,还有感谢 Hans Dockter 给 Ben 这个时间。 - -感谢 Jeremy Cerise 和 Bill Frasure 来到开发商务虚会预订,并随后提供了宝贵的帮助。 - -感谢所有花时间和精力来Crested Butte, Colorado(科罗拉多州克雷斯特德比特)镇参加我的研讨会,开发商务聚会和其他活动的人!你们的贡献可能没被轻易看到,但它非常重要! - - - - -## 献礼 - -> 谨以此书献给我敬爱的父亲 E. Wayne Eckel。 -> 1924年4月1日至2016年11月23日 - - - diff --git a/book/01-What-is-an-Object.md b/book/01-What-is-an-Object.md deleted file mode 100644 index 03208fcb..00000000 --- a/book/01-What-is-an-Object.md +++ /dev/null @@ -1,304 +0,0 @@ - -[TOC] - - -# 第一章 对象的概念 - - -> "我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们,语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。" -- Alfred Korzybski (1930) - -计算机革命的起源来自机器。编程语言就像是那台机器,它不仅是我们思维的放大工具和另一种表达媒介,更像我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。 - -面向对象编程(*Object-oriented programming OOP*)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对不太理解,可先行跳过本章。等你对编程具有一定理解后,请务必再回过头来看。因为这样你才能理解面对对象编程的重要性以及如何使用它来设计你的程序。 - -## 抽象 - -所有编程语言都提供一种"抽象"的方法。从某种程度上来说,解决问题的复杂性与抽象的种类和质量直接相关。这里的"种类"意思是:你准备对什么进行抽象?汇编语言是对机器底层的一种少量抽象。后来的许多"命令式"语言(如 FORTRAN,BASIC 和 C)是对汇编语言的一种抽象。与汇编语言相比,这些语言已有了长足的进步,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。 - -在机器模型("解决方案空间")与实际解决的问题模型("问题空间")之间,程序员必须建立起一种关联。这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门完善的"编程方法"学科。 - -为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL,它们的做法是"从不同的角度观察世界"——"所有问题都归纳为列表"或"所有问题都归纳为算法"。PROLOG 则将所有 -问题都归纳为决策链。对于这些语言,我们认为它们一部分是"基于约束"的编程,另一部分则是专为 -处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。 - -面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达"问题空间"内的元素。由于这种 -表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示物称作"对象"(Object)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据方案。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的"对象"或者"物体"相比,编程"对象"与它们也存在共通的地方:它们都有自己的特征和行为。 - -Smalltalk 作为第一种成功的面向对象程序设计语言和 Java 的基础语言,Alan Kay 总结了其五大基本特征。通过这些特征,我们可理解"纯粹"的面向对象程序设计方法是什么样的: - -> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它可以存储数据,可以在你对其"发出请求"时执行本身的操作。理论上讲,你可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表达为一个对象。 -> 2. **程序是一组对象,通过信息传递来告知彼此该做什么**。要请求一个对象,你需要向该对象发送信息。 -> 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。 -> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个"类"的一个"实例"。其中,"类"(Class)是"类型"(Type)的同义词。一个类最重要的特征就是"能将什么信息发给它?"。 -> 5. **同一类所有对象都能接收相同的信息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为"圆"(Circle)的一个对象也属于类型为"形状"(Shape)的一个对象,所以一个圆完全能接收形状信息。这意味着可让程序代码统一指挥"形状",令其自动控制所有符合"形状"描述的对象,其中自然包括"圆"。这一特性称为对象的"可替换性",是OOP最重要的概念之一。 - -Grady Booch 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和身份。这意味着对象有自己的内部数据(由状态提供)、方法 (由特性提供),并彼此区分(每个对象在内存中都有唯一的地址)。 - - -## 接口 - -亚里士多德(*Aristotle*)或许是认真研究"类型"概念的第一人,他曾谈及"鱼类和鸟类"的问题。是世界首例面向对象语言Simula-67 中,第一次用到了这样的一个概念: -所有对象——尽管各有特色——都属于某一系列对象的一部分,这些对象具有通用的特征和行为。在Simula-67 中,首次用到了`class` 这个关键字,它为程序引入了一个全新的类型(`class` 和 type 通常可互换使用,有些人进行了进一步的区分,他们强调"类型"决定了接口,而"类"是那个接口的一种特殊实现方式)。 - -Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是"模拟"(Simulate)象"银行出纳员"这 -样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一 -些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有 -自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实 -体分别表示出纳员、客户、帐号以及交易。这个实体便是"对象",而且每个对象都隶属一个特定的"类",那个类具有自己的通用特征与行为。 - -因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据"类型"(Type),但几乎所 -有面向对象的程序设计语言都采用了`class`关键字。当您看到"type"这个字的时候,请同时想到`class`;反之亦然。 - -创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在"问题空间"(问题实际存 -在的地方)的元素与"方案空间"(对实际问题进行建模的地方,如计算机)的元素之间建立理想的"一对 -一"的映射关系。 - -那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的"接口"(Interface)定义的,对象的"类型"或"类"则规定了它的接口形式。"类 -型"与"接口"的对应关系是面向对象程序设计的基础。 - -下面让我们以电灯泡为例: - -![reader](../images/reader.png) - -```java -Light lt = new Light(); -lt.on(); -``` - -在这个例子中,类型/类的名称是 Light,可向 Light 对象发出的请求包括包括打开(on)、关闭(off)、变得更明亮(brighten)或者变得更暗淡(dim)。通过简单地声明一个名字(lt),我们为 Light 对象创建了一个"句柄"。然后用new关键字新建类型为 Light 的一个对象。再用等号将其赋给句柄。 - -为了向对象发送一条信息,我们列出句柄名(lt),再用一个句点符号(.)把它同信息名称(on)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。 - -上图遵循 UML(*Unified Modeling Language*,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的信息)在框的底部。通常,只有类的名称和公共方法在 UML 设计图中显示,因此中间部分未显示,如本例所示。如果您只对类名感兴趣,则也不需要显示方法信息。 - - -## 服务提供 - -在开发或理解程序设计时,我们可以将对象看成是"服务提供者"。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。 - -那么问题来了:我们该选择哪个对像来解决问题呢? 例如,你正在开发一个记事本程序。你可能会想在屏幕输入一个默认的记事本对象,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象? - -我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。 - - -## 封装 - -我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否按照正确的规则来使用或是改变了该工具类。只有设定访问控制,才能从根本上阻止。 - -因此,使用访问控制的原因有以下2点: - -1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。); - -2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松的完成改造。 - -Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些修饰符可以明确谁能访问后面的方法、变量或类。 - - 1. `public` (公开) 表示任何人都可以访问和使用该元素; - - 2. `private` (私有) 除了类本身,外界无法直接访问该元素和内部方法。`private`是你和调用者之间的屏障。任何试图访问私有成员的人都会收到编译时错误; - - 3. `protected` (受保护) 类似于`private`,区别是继承类(下一节就会引入继承的概念)可以访问`protected`的成员,但不能访问`private`成员; - - 4. `default` (默认) 如果你不使用前面的三者,默认就是`default`访问权限。`default`被称之为包访问,因为该权限下的资源可以同一包(库组件)中其他类的成员访问。 - - -## 复用 - -一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。 - -代码和设计方案的复用性是面向对象的程序设计的优点之一。我们可以通过重复使用某个`class`来达到这种复用性。同时,我们也可以将这个`class`作为另一个`class`的成员变量来使用。新的对象可以是由任意数量、类型的其他对象构成。这里涉及到"组合"和"聚合"的概念: - -* **组合**(*Composition*) 经常用来表示"拥有"关系(*Has-a Relationship*)。例如,"汽车拥有了主机"。 - -* **聚合**(*Aggregation*) 动态的**组合**。 - -![UML-example](../images/1545758268350.png) - -上图中实心棱形指向"**Car**"表示**组合**的关系;如果是**聚合**关系,可以使用空心棱形。 - -(**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时间共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。) - -使用"组合"关系会为我们的程序带来极大的灵活性。通常新构建的`class`"成员对象"会使用`private`访问权限,这样应用程序员则无法对其直接访问。我们就可以在不干扰客户代码的前提下,从容地修改那些成员。也可以在"运行期"更改成员,这进一步增大了灵活性。下面一节要讲到的"继承"并不具备这种灵活性,因为编译器必须对通过继承创建的类加以限制。 - -在面向对象编程中经常重点强调"继承"。在新程序员的印象里,或许早已先入为主地认为"继承应当随处可见"。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑"组合",因为它更简单灵活,并且设计逻辑清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。 - -## 继承 - -"继承"给面向对象编程带来极大的便利。它在概念上允许我们将各式各样数据和功能封装到一起。这样便可恰当表达"问题空间"的概念,而不是强制使用底层机器的习惯用法。 - -通过使用`class`关键字,这些概念在编程语言中表示为基本单元。但若能利用现成的数据类型,对其进行"克隆",再根据情况进行添加和修改,情况就显得理想多了。"继承"正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基础类、超类或父类)发生了变化,修改过的"克隆"类(正式名称叫作继承类或者子类)也会反映出这种变化。 - -如果我们可以采用现有的类,克隆它,然后对克隆进行添加和修改,那就更好了。这实际上是通过继承得到的,只是如果原始类(称为基类或超类或父类)发生了更改,修改的"克隆"(称为派生类或继承类或子类或子类)也反映了这些更改。 - -![Inheritance-example](../images/1545763399825.png) - -这个图中的箭头从派生类指向基类。正如您将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的信息(或者以不同的方式处理它们)。继承通过基本类型和派生类型的概念来表达这种相似性。基类型包含派生自它的类型之间共享的所有特征和行为。创建基本类型以表示思想的核心。从基类型中,可以派生出其他类型来表示实现该核心的不同方式。 - -![1545764724202](../images/1545764724202.png) - -例如,垃圾回收机对垃圾进行分类。基本类型是"垃圾"。每块垃圾都有重量、值等,并且可以被切碎、熔化或分解。由此,可以衍生出更具体的垃圾类型,这些垃圾具有附加特征(瓶子有颜色,钢罐有磁性)或行为(可以压碎铝罐)。此外,一些行为可以不同(纸张的价值取决于它的类型和条件)。使用继承,您将构建一个类型层次结构,该层次结构根据类型表达您试图解决的问题。第二个例子是常见的"形状"例子,可能用于计算机辅助设计系统或游戏模拟。基本类型是"形状",每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以导出(继承)特定类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。 - -![1545764780795](../images/1545764780795.png) - -例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积时。类型层次结构体现了形状之间的相似性和差异。以与问题相同的术语转换解决方案是有用的,因为您不需要中间模型来从问题的描述获得解决方案的描述。对于对象,类型层次结构是模型的一个重要方面,因此您可以直接从真实世界中的系统描述转到代码中的系统描述。的确,有时候,那些被训练去寻找复杂解决方案的人在面向对象设计的简单性方面有困难。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),更重要的是它复制了基类的接口。也就是说,基类对象接受的所有信息也被派生类对象接受。根据类接受的信息,我们知道类的类型,因此派生类与基类是相同的类型。 - -![1545764820176](../images/1545764820176.png) - -在前面的例子中,"圆是形状"。这种通过继承的类型等价是理解面向对象编程含义的基本网关之一。因为基类和派生类都具有相同的基本接口,所以必须有一些实现来支持该接口。也就是说,当对象接收到特定信息时,必须有可执行代码。如果继承一个类并且不做其他任何事情,则来自基类接口的方法直接进入派生类。这意味着派生类的对象不仅具有相同的类型,而且具有相同的行为,这并不特别有趣。有两种方法可以区分新派生类与原始基类。第一种方法很简单:向派生类添加全新的方法。这些新方法不是基类接口的一部分。这意味着基类没有按照您想要的那样多,所以您添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的方法。 - - -## 多态 - -我们在处理类的层次结构时,通常是定义对象的基类而不是对象本身。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个"形状"的图例中,"方法"(method)操纵的是通用"形状",而不关心它们是"圆"、"正方形"、"三角形"还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此"方法"向其中的任何代表"形状"的对象发送信息都不必担心对象如何处理信息。 - -这样代码不受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 举个例子来说吧:你可以通过这个通用的"形状"基类来得到一个新的"五角星"形状得子类。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。 - -这样的设计减少了程序的维护难度。我们把派生的对象类型统一看成是它本身的基础类型("圆"是一种"形状","自行车"是"车","鸬鹚"也是"鸟"等等)。编译器(compiler)在编译时期无法精确的知道什么"形状"被擦除,哪一种"车"在行驶,或者是某种"鸟"在飞行。当程序接收这种信息之前程序员并不知道哪段代码会被执行。"擦除"的方法可以平等地应用到每一种可能的"形状"上。 - -如果不需要考虑执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要特殊的代码来确定 **Bird** 工作的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、苍蝇或游泳、企鹅走路或游泳)的呢? - -![Bird-example](../images/1545839316314.png) - -答案是继承的主要转折点:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器生成的函数调用生成所谓的早期绑定,这个术语您可能从未听说过,因为您从未以其他方式考虑过。这意味着编译器生成对特定函数名的调用,该调用解析为要执行的代码的绝对地址。 - -通过继承,程序直到运行时才能确定代码的地址,因此当信息被发送到对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用后期绑定的概念。当向对象发送信息时,调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。 - -为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当您向对象发送信息时,该对象实际上确定如何处理该信息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++使用虚拟关键字。在这些语言中,默认情况下方法没有动态绑定。在 Java 中,动态绑定是默认行为,不需要额外的关键字来生成多态性。 - -为了演示多态性,我们编写了一段代码,它忽略了类型的特定细节,只与基类对话。该代码与特定于类型的信息分离,因此更易于编写和更容易理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。 - -代码示例: - -```java -void doSomething(Shape shape) { - shape.erase(); - // ... - shape.draw(); -} -``` - -此方法与任何 Shape 都相关,因此它独立于所绘制和擦除的对象的特定类型。此时程序的其他部分使用**doSomething()** 方法: - -```java - Circle circle = new Circle(); - Triangle triangle = new Triangle(); - Line line = new Line(); - doSomething(circle); - doSomething(triangle); - doSomething(line); - -``` - -可以看到无论传入的"形状"是什么,程序都正确的执行了。 - -![shape-example](../images/1545841270997.png) - -这实际是一个非常有用的编程技巧。分析下面这行代码: - -```java - doSomething(circle); -``` -这里将 **Circle**(圆)句柄传递给一个本来期待 **Shape**(形状)句柄的方法。由于圆也是一种几何形状,所 -以 **doSomething(circle)** 能正确地执行。也就是说,**doSomething()** 能接受任意 **Shape** 的信息。这是完全安全和合乎逻辑的事情。 - -这种把子类当成其基类来处理的过程叫做"向上转型"(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 **doSomething()** 代码示例: - -```java - shape.erase(); - // ... - shape.draw(); - -``` - -我们可以看到程序并未这样表达:"如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做;等等"。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:"你是一种几何形状,我知道你能将自己删掉,即 erase();请自己采取具体行动,并控制所有的细节吧。" - -尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用draw()时执行的代码与为一个 Square 或 Line 调用 draw() 时执行的代码是不同的。但在将 draw() 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 doSomething() 编译代码时,它并不知道自己要操作的准确类型是什么。 - -尽管我们确实可以保证最终会为 Shape 调用 erase()、 draw(),但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢? - -将信息发给对象时,如果程序不知道接受的具体类型是什么,但最终执行是正确的,这就是对象的"多态性"(**Polymorphism**)。面向对象的程序设计语言是通过"动态绑定"的方式来实现对象的多态性的。编译器和运行期系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好的设计程序。 - -## 单继承 - -自从 C++ 引入以来,OOP 问题变得尤为突出。是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的。(实际上,除 C++ 以外的其他虚拟机语言也是这样。)在 Java 中这个最终的基类的名字就是`Object`。 - -Java 的单继承结构有很多好处。由于所有对象都有继承自一个公共接口,因此它们最终都属于同一个基本类型。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个的基类。这种方案的限制更少一点。从向后兼容的角度看,多继承的方案更符合 C 的模型。 - -对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的"灵活性"值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。 - -另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。 - -由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如[异常处理](#异常处理)。同时,这也让我们的编程具有更大的灵活性。 - - -## 集合 - -通常,我们并不知道解决某个具体问题需要的对象数量,持续时间,以及对象的存储方式。那么我们如何知悉程序在创建时需要分配的内存空间呢? - -在面向对象的设计中,问题的解决方案有些千篇一律:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的"数组"(**Array**)。在 Java 中"集合"(**Collection**)的使用率更高。(也可称之为"容器",但"集合"这个称呼更通用。) - -"集合"这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。 - -还好,一般优秀的 OOP 语言都会将"集合"作为其基础包。在 C++ 中,"集合"是其标准库的一部分。通常被称为 STL(标准模板库,*the Standard Template Library*)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。在一些库中,一两个集合泛型就能满足我们所有的需求了。在 Java 中不同的需求对应不同种类的集合 - -在一些库中,一个或两个泛型集合被认为是对所有需求都足够好的,而在其他(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(*Queue*)、树(*Tree*)、堆(*Stack*)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因: - -1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,为我们解决问题提供了灵活的方案。 - -2. 不同的集合种类对应着不同的用途。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。 - -通过对 List 接口的抽象,我们可以很容易的将 LinkedList 改为 ArrayList。在 Java 5泛型出来之前,集合中保存的是通用类型`Object`。Java 单继承的结构意味着所有元素都基于`Object`类,所以在集合中可以保存任何类型的数据。这也使得集合易于重用。要使用这样的集合时,我们先要往集合添加元素。由于 Java 5版本前的集合只保存`Object`,当我们往集合中添加元素时,元素便向上转型成了`Object`,从而丢失自己原有的类型特性。这时我们再从对象中取出该元素时,元素的类型变成了`Object`。那么我们该怎么将其转回原先具体的类型的?这里,我们使用了强制类型转换将其转为更具体的类型。这个过程称之为对象的"向下转型"。通过"向上转型",我们知道"圆形"也是一种"形状",这个过程是安全的。可是我们不能从"Object"看出其就是"圆圈"或"形状",所以除非我们能确定元素的具体类型信息,否则"向下转型"就是不安全的。也不能说这样的错误就时完全危险的,因为一旦我们转化了错误的类型,程序就会运行出错,抛出"运行时异常"(*RuntimeException*)。(后面的章节会提到) 无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的"向下转型"对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素"向下转型"中的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(*Parameterized Type Mechanism*)。 - -参数化类型机制可以使得编译器能够自动识别某个`class`的具体类型并正确地执行. 举个例子,对集合的参数化类型机制可以让其仅接受"形状"这种类型的元素,并以"形状"类型取出元素。Java 5版本支持了参数化类型机制,称之为"泛型"(*Generic*)。泛型是 Java 5的主要特性之一。举个例子,你可以按以下方式向 ArrayList 种添加 Shape(形状): - -```java - ArrayList shapes = new ArrayList(); -``` - -泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。 - - -## 生命周期 - -我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时我们应该及时释放资源,清理内存。 - -在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂: - -假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个"飞机"对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统。这些数据的重要性靠后,可以放在系统空闲的时候再后台处理。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个"飞机"对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。 - -现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。 - -这个对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在堆栈(*Stack*,有时称为自动变量或作用域变量)或静态存储区域(*static storage area*)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。 - -然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。 - -第二种方法是在堆内存(*Heap*)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟空间通常是一条将栈指针向下移动,另一条将其向后移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。 - -动态方法使一般的逻辑假设对象趋于复杂,因此额外的内存查找和释放的开销对对象的创建影响不大。(原文:*The dynamic approach makes the generally logical assumption that objects tend to be complicated, so the extra overhead of finding storage and releasing that storage will not have an important impact on the creation of an object.*)此外,更好的灵活性对于问题的解决至关重要。 - -Java 使用动态内存分配。每次创建对象时,使用`new`关键字构建该对象的动态实例。这又带来另一个问题:对象的生存周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象,否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这使得许多 C++ 项目没落。 - -Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不包括对象清理的其他方面)。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制,Java 的编码过程较之 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多! - - -## 异常处理 - -自编程语言被发明以来,程序的错误处理一直都是个难题,因为很难设计出一个好的错误处理方案。许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。 - -异常处理机制将程序错误直接交给编程语言甚至是操作系统。"异常"(*Exception*)是一个从出错点"抛出"(*thrown*)后能被特定类型的异常处理程序捕获(*catch*)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,如果**throw**的异常类型和**catch**的不符,则不会触发该条件下的异常处理程序。异常的发生是不会被忽略的,它终究会在某一时刻被处理。 - -最后,"异常机制"提供了一种可靠地从意外情况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。 - -Java 的异常处理机制在编程语言中脱颖而出。在 Java 中,异常处理从一开始就被连接起来,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种保证错误一致性的方法有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。 - - -## 本章小结 - -面向过程程序包含数据定义和函数调用。要找到程序的意图,你必须要在脑中建立一个模型,弄清函数调用和更底层的概念。这些程序往往容易混淆,因为表达式的术语更多地面向计算机而不是我们要解决的问题。这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。 - -你看到的是对象的概念,这些概念是站在"问题空间"的(而不是站在计算机角度的"解决方案空间"),以及发送给对象以指示该空间中的活动的信息。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。 - -OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来,Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python)。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。 - - - diff --git a/book/02-Installing-Java-and-the-Book-Examples.md b/book/02-Installing-Java-and-the-Book-Examples.md deleted file mode 100644 index 812ac1f2..00000000 --- a/book/02-Installing-Java-and-the-Book-Examples.md +++ /dev/null @@ -1,191 +0,0 @@ -[TOC] - -# 第二章 安装Java和本书用例 - -现在,我们来为这次阅读之旅做些准备吧! - -在开始学习 Java 之前,你必须要先安装好 Java 和本书的源代码示例。因为考虑到可能有"专门的初学者"从本书开始学习编程,所以我会仔细解释计算机命令行 Shell 的这个过程。 如果你已经有此方面的经验了,可以跳过这段安装说明。如果你对此处描述的任何术语或过程仍不清楚,还可以通过 Google 搜索找到答案。具体的问题或困难请试着在 StackOverflow 上提问。或者去 YouTube 看有没有相关的安装说明。 - - -## 编辑器 - -首先你需要安装一个编辑器来创建和修改本书用例里的 Java 代码。有可能你还需要一个编辑器来更改系统配置文件。 - -相比一些重量级的 IDE(Integrated Development Environments,开发集成环境)软件,如Eclipse、NetBeans和IntelliJ IDEA (译者注:做项目强烈推荐IDEA),编辑器是一种基础的运行程序的文本编辑器。如果你已经有了一个 IDE 用着还顺手,那就可以直接用了。为了方便后面的学习和统一下教学环境,我推荐大家使用 Atom 这个编辑器。大家可以在 [atom.io](http://atom.io) 网站下载。 - - Atom 是一个免费开源、易于安装且跨平台(支持 Window、Mac和Linux)的文本编辑器。内置支持 Java 文件。相比 IDE 的厚重,她比较轻量级,是学习本书的理想工具。Atom 包含了许多方便的编辑功能,相信你一定会爱上她!更多关于 Atom 使用的细节问题可以到她们的网站上。 - -还有很多其他的编辑器。有一种亚文化的群体,他们热衷于争论哪个更好用!如果你找到一个你更喜欢的编辑器,换一种使用也没什么难度。重要的是,你要找一个用着舒服的。 - - -## Shell - -如果你之前没有接触过编程,那么有可能对 Shell(命令行窗口) 不太熟悉。shell 的历史可以追溯到早期的计算时代,当时在计算机上的操作是都通过输入命令进行的,计算机通过回显响应。所有的操作都是基于文本的。 - -尽管和现在的图形用户界面相比,Shell 操作方式很原始。但是同时 shell 也为我们提供了许多有用的功特性。在学习本书的过程中,我们会经常使用到 Shell,包括现在这部分的安装,还有运行 Java 程序。 - -Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入"terminal"。单击看起来像小电视屏幕的应用程序(您也可以单击"return")。这就启动了你的用户下的 shell窗口。 - - windows:首先,通过目录打开 windows 资源管理器: - Windows 7: 单击屏幕左下角的"开始"图标,输入"explorer"后按回车键。 - Windows 8: 按 Windows+Q, 输入 "explorer" 后按回车键。 - Windows 10: 按 Windows+E 打开资源管理器,导航到所需目录,单击窗口左上角的"文件"选项卡,选择"打开 Window PowerShell"启动 Shell。 - Linux: 在 home 目录打开 Shell。 - Debian: 按 Alt+F2, 在弹出的对话框中输入"gnome-terminal" - Ubuntu: 在屏幕中鼠标右击,选择 "打开终端", 或者按住 Ctrl+Alt+T - Redhat: 在屏幕中鼠标右击,选择 "打开终端" - Fedora: 按 Alt+F2,在弹出的对话框中输入"gnome-terminal" - -**目录** -目录是 Shell 的基础元素之一。目录用来保存文件和其他目录。目录就好比树的分支。如果书籍是您系统上的一个目录,并且它有两个其他目录作为分支,例如数学和艺术,那么我们就可以说你有一个书籍目录里,它包含数学和艺术两个子目录。注意:Windows 使用"\"而不是"/"来分隔路径。 - -**Shell基本操作** -我在这展示的 Shell 操作和系统中大体相同。出于本书的原因,下面列举一些在 Shell 中的基本操作: - -```shell -更改目录: cd <路径> - cd .. 移动到上级目录 - pushd <路径> 记住来源的同时移动到其他目录,popd 返回上一个目录 - -目录列举: ls 列举出当前目录下所有的文件和子目录名(不包含隐藏文件), - 可以选择使用通配符 * 来缩小搜索的范围。 - 示例(1): 列举所有以".java"结尾的文件,输入ls *.java - 示例(2): 列举所有以"F"开头,".java"结尾的文件,输入ls F*.java - -增加目录: - Mac/Linux 系统:mkdir - 示例:mkdir books - Windows 系统:md - 示例:md books - -移除文件: - Mac/Linux 系统:rm - 示例:rm somefile.java - Windows 系统:del - 示例:del somefile.java - -移除目录: - Mac/Linux 系统:rm -r - 示例:rm -r somefile.java - Windows 系统:deltree - 示例:deltree somefile.java - -重复命令: !! 重复上条命令 - 示例:!n 重复倒数第n条命令 - -命令历史: - Mac/Linux 系统:history - Windows 系统:按 F7 键 - -文件解压: - Linux/Mac 都有命令行解压程序,您可以通过互联网为Windows安装命令行解压程序。 - 图形界面下(Windows 资源管理器、Mac Finder ,Linux Nautilus 或其他等效软件)右键单击该文件, - 在 Mac 上选择"open",在 Linux 上选择"extract here",或在 Windows 上选择"extract all..."。 - 要了解关于shell 的更多信息,请在维基百科中搜索 Windows shell,Mac/Linux用户可搜索 bash shell。 - -``` - - -## Java安装 - -为了安装了和运行代码示例,首先你必须安装 JDK 1.8。本书中采用的是 JDK 1.8版本。 - - -**Windows** - -1. 以下为 Chocolatey 的[安装说明](https://chocolatey.org/)。 -2. 在命令行提示符下输入下面的命令,等待片刻,结束后 Java 安装完成并自动完成环境变量设置。 - -```shell - choco install jdk8`. -``` - -**Macintosh** - -Mac 系统自带的 Java 版本太老,为了确保本书的代码示例能被正确执行,你必须先更新它到 Java 8。我们需要管理员权限来运行下面的步骤: -1. 以下为 HomeBrew 的[安装说明](https://brew.sh/)。安装完成后执行命令 `brew update` 更新到最新版本 -2. 在命令行下执行下面的命令来安装 Java。 - -```shell - brew cask install java -``` - -当以上安装都完成后,如果你有需要,可以使用游客账户来运行本书中的代码示例。 - -**Linux** - -* **Ubuntu/Debian**: - -```shell - sudo apt-get update - sudo apt-get install default-jdk -``` -* **Fedora/Redhat**: - -```shell - su-c "yum install java-1.8.0-openjdk" -``` - - -## 校验安装 - -打开新的命令行输入: - -```shell -java -version -``` - -正常情况下 你应该看到以下类似信息(版本号信息可能不一样): - -```shell -java version "1.8.0_112" -Java(TM) SE Runtime Environment (build 1.8.0_112-b15) -Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode) -``` -如果提示命令找不到或者无法被识别,请根据安装说明重试;如果还不行,尝试到 [StackOverflow](https://stackoverflow.com/search?q=installing+java) 寻找答案。 - - -## 安装和运行代码示例 - -当 Java 安装完毕,下一步就是安装本书的代码示例了。安装步骤所有平台一致: - -1. 在 [GitHub 仓库](https://github.com/BruceEckel/OnJava8-Examples/archive/master.zip)中下载本书代码示例 -2. 解压到你所选目录里。 -3. 使用 Windows 资源管理器, Mac Finder, or Linux 的 Nautilus 或其他等效工具浏览,在该目录下打开 Shell 命令行。 -4. 如果你在正确的目录中,你应该看到该目录中名为 gradlew 和 gradlew.bat 的文件,以及许多其他文件和目录。目录与书中的章节相对应。 -5. 在命令行中输入下面的命令运行: - -```shell - Windows 系统: - gradlew run - - Mac/Linux 系统: - ./gradlew run -``` - -第一次安装时 Gradle 需要安装自身和其他的相关的包,请稍等片刻。安装完成后,后续的安装将会快很多。 - -**注意**: 第一次运行 gradlew 命令时必须连接互联网。 - - -**Gradle基础任务** - -本书构建的大量 Gradle 任务都可以自动运行。Gradle 设置使用约定大于配置的方式,简单设置即可具备高可用性。本书中"一起去骑行"的某些任务不适用于此或无法执行成功。以下是你通常会使用上的分级任务列表: - -```java - 编译本书中的所有 java 文件,除了部分错误示范的 - gradlew compileJava - - 编译并执行 java 文件(某些文件是库组件) - gradlew run - - 执行所有的单元测试(在本书学习中校验自己的代码是由正确) - gradlew test - - 编译且运行一个特别的示例程序 - gradlew <本书章节>:<示例名称> - 示例:gradlew objects:HelloDate -``` - - - diff --git a/book/03-Objects-Everywhere.md b/book/03-Objects-Everywhere.md deleted file mode 100644 index 58ada688..00000000 --- a/book/03-Objects-Everywhere.md +++ /dev/null @@ -1,636 +0,0 @@ -[TOC] - -# 第三章 万物皆对象 - ->如果我们说不同的语言,我们会感觉到一个不同的世界!— Ludwig Wittgenstein (1889-1951) - -尽管 Java 基于 C++ ,但 Java 是一种更纯粹的面向对象程序设计语言。Java 和 C++ 都是混合语言。在 Java 中,语言设计者认为混合并不像 C++ 那样重要。混合语言允许多种编程风格;这也是 C++ 支持与 C 语言的向后兼容性原因。因为 C++ 是 C 语言的超集,所以它也包含了许多 C 语言的不良特性,这可能使得 C++ 在某些方面过于复杂。 - - Java 语言预设你已经编写过面对对象的程序。在此之前,你必须将自己的思维置于面对对象的世界。在本章中你将了解 Java 语言的基本组成,学习 Java (几乎)万物皆对象的思想。 - - -## 对象操纵 - -名字的意义在于,当我们听到"玫瑰"这个词时,就会想到一种闻起来很甜蜜的的花。(引用自 莎士比亚,《罗密欧与朱丽叶》)。 - -所有的编程语言都会操纵内存中的元素。有时程序员必须要有意识地直接或间接地操纵它们。示例:在 C/C++ 语言中是通过指针来完成操作的。 - -Java 使用万物皆对象的思想和特有的语法方式来简化问题。虽然万物皆可为对象,但你操纵的标识符实际上是只对象的"引用" [^1]。 示例:我们可以将这种"引用"想象成电视(对象)和遥控器(引用)之间的关系。只要拥有对象的"引用",就可以操纵该"对象"。我们无需直接接触电视,只要掌握遥控器就可以在房中自由地控制电视(对象)的频道和音量。此外,没有电视机,遥控器也可以单独存在。引申来说,仅仅因为你有一个"引用"并不意味着你必然有一个关联的"对象"。 - -下面来创建一个 String 的引用,用于保存单词语句。代码示例: - -```java - String s; -``` - -这里我们仅仅只是创建了一个 String 对象的引用,而非对象。直接拿来使用会出现错误:因为此时你并没有给变量 s 赋值--附加任何引用的对象。通常更安全的做法是:在声明变量引用的同时初始化对象信息。代码示例: - -```java - String s = "asdf"; -``` - -Java 语法允许我们使用带双引号的文本内容来初始化字符串。同样,其他类型的对象也有相应的初始化方式。 - - -## 对象创建 - -"引用"用来连接"对象"。在 Java 中,通常我们使用`new`这个操作符来创建一个新的对象。`new`关键字代表:创建一个新的对象实例。所以,前面的代码实例我们也可以这样来表示: - -```java - String s = new String("asdf"); -``` -以上的代码示例展示了字符串对象的创建过程,以及如何初始化生成字符串。Java 本身自带了许多现成的数据类型,在此基础之上我们还可以创建自己的数据类型。类型的创建是 Java 的基本操作。在本书后面的学习中将会接触到。 - - -### 数据存储 - -那么, 程序在运行时是如何存储的呢?尤其是内存。下面我们就来形象地描述下, Java 中数据存储的5个不同的地方: - -1. **寄存器** (*Registers*) 最快的保存区域,位于CPU内部 [^2]。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。 - -2. **栈内存**(*Stack*) 存在于常规内存(RAM)区域中,可通过栈指针获得处理器的直接支持。栈指针下移创建新内存,上移释放该内存,顺序后进先出,速度仅次于寄存器。创建程序时,Java 编译器必须准确地知道栈内保存的所有数据的"长度"以及生命周期。栈内存的这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象本身却是保存在堆内存的。 - -3. **堆内存**(*Heap*) 这是一种常规用途的内存池(也在 RAM区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 `new` 命令实例化代码即可。执行这些代码时,数据会在堆内存里自动进行保存。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果你甚至可以用 Java 在栈内存上创建对象,就像在C++ 中那样)。随着时间的推移,Java 的堆内存分配机制现已非常快,因此这不是一个值得关心的问题了。 - -4. **常量存储** (*Constant storage*) 常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器(ROM)中 [^3]。 - -5. **非 RAM 存储** (*Non-RAM storage*) 数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复到常规内存中。Java 为轻量级持久性提供支持。诸如 JDBC 和 Hibernate 之类的库为使用数据库存储和检索对象信息提供了更复杂的支持。 - - - -### 基本类型的存储 - -有一组类型在 Java 中使用频率很高,这就是 Java 的基本类型。对于此类数据的存储我们需要特别对待。之所以说这么说,是因为它们的创建并不是通过 `new` 关键字来产生。通常 `new` 出来的对象都是保存在 **Heap** 内存中的, 用它来创建小、简单的基本类型的数据是不划算的。所以对于这些基本类型的创建方法, Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 `new` 创建变量,而是使用一个"自动"变量。 这个变量容纳了具体的值,并置于栈内存中,能够更高效地存取。 - -Java 预设了每种基本类型的初始内存占用大小。 这些大小标准不会随着机器环境的变化而变化。这种不变性也是Java 的跨平台的一个原因。 - -| 基本类型 | 大小 | 最小值 | 最大值 | 包装类型 | -| :------: | :------: | :------: | :------: | :------: | -| boolean | — | — | — | Boolean | -| char | 16 bits | Unicode 0 | Unicode 2ドル^{16}-1$ | Character | -| byte | 8 bits | $-128$ | $+127$ | Byte | -| short | 16 bits | $-2^{15}$ | $+2^{15}-1$ | Short | -| int | 32 bits | $-2^{31}$ | $-2^{31}-1$ | Integer | -| long | 64 bits | $-2^{63}$ | $-2^{63}-1$ | Long | -| float | 32 bits | IEEE754 | IEEE754 | Float | -| double | 64 bits |IEEE754 | IEEE754 | Double | -| void | — | — | — | Void | - -所有的数值类型都是有正/负符号的。布尔(boolean)类型的大小没有明确的规定,通常定义为采用文字 "true" 和 "false"。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例: - -```java -char c = 'x'; -Character ch = new Character(c); -``` -或者你也可以使用下面的形式 基本类型自动转换成包装类型(自动装箱): - -```java -Character ch = new Character('x'); -``` -相对的,包装类型转化为基本类型(自动拆箱): - -```java -char c = ch; -``` - -个中原因将在以后的章节里解释。 - - -### 高精数值的存储 - -在 Java 中有两种类型的数据可用于高精度的计算。它们是 `BigInteger` 和 `BigDecimal`。尽管它们大致可以划归为"包装类型",但是它们并没有相应的基本类型形式。 - -这两个类都有自己特殊的"方法",对应于我们针对基本类型数值执行的操作。也就是说,能对 int 或 float 做的运算,在 BigInteger 和 BigDecimal 这里也同样可以做一样可以,只不过必须要通过调用它们的方法来实现而非运算符。此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。 - -BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。 -BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的币值计算。至于具体使用什么方法,更多详情,请参考 JDK 官方文档。 - - - -### 数组的存储 - - -许多编程语言都支持数组类型。在 C 和 C++ 中使用数组是危险的,因为那些数组只是内存块。如果程序访问了其内存块之外的数组或在初始化之前使用该段内存(常见编程错误),则结果是不可预测的。 - -Java 的设计主要目标之一是安全性,因此许多困扰 C 和 C++ 程序员的问题不会在 Java 中再现。在 Java 中,数组使用前需要被初始化,并且不能访问数组长度以外数据。这种长度检查的代价是每个阵列都有少量的内存开销以及在运行时验证索引的额外时间,但是这种安全性的前提对于提高的生产率是值得的。(并且 Java 经常可以优化这些操作)。 - -当我们创建对象数组时,实际上是创建了一个数据的引用,并且每个引用的初始值都为 **null** 。在使用该数组之前,我们必须为每个引用分配一个对象 。如果我们尝试使用为**null**的引用,则会在运行时报告该问题。因此,在 Java 中就防止了数组操作的典型错误。 - -我们还可创建基本类型的数组。编译器通过将该数组的内存归零来保证初始化。本书稍后将详细介绍数组,特别是在数组章节中。 - - - - -## 代码注释 - - -Java 中有两种类型的注释。第一种是传统的 C 风格的注释,以 `/*` 开头,可以跨越多行,到 `*/ ` 结束。**注意**,许多程序员在多行注释的每一行开头添加 `*`,所以你经常会看到: - -```java -/* This is a comment -* that continues -* across lines -*/ -``` - -但请记住, `/*` 和 `*/` 之间的内容都是被忽略的。所以你将其改为下面的风格也是没有区别的。 - -```JAVA -/* This is a comment that -continues across lines */ -``` - -第二种注释形式来自 C++ 。它是单行注释,以 `//` 开头并一直持续到行结束。这种注释方便且常用,因为它很直观和简单。所以你经常看到: - -```JAVA -// 这是单行注释 -``` - - -## 对象清理 - -在一些编程语言中,管理存储的生命周期需要大量的工作。一个变量需要存续多久?如果我们想销毁它,应该什么时候去做呢?存储生命周期的混乱会导致许多错误,本小结将会向你介绍 Java 是如何通过释放存储来简化这个问题的。 - - -### 作用域 - -大多数程序语言都有作用域的概念。这将确定在该范围内定义的名称的可见性和生存周期。在 C、C++ 和 Java 中,作用域是由大括号 `{}` 的位置决定的。下面是 Java 代码作用域的一个示例: - -```JAVA -{ - int x = 12; -// 仅 x 变量可用 -{ - int q = 96; -// x 和 q 变量皆可用 -} -// 仅 x 变量可用 -// 变量 q 不在作用域内 -} -``` - -Java 的变量只有在其作用域内才可用。缩进使得 Java 代码更易于阅读。由于 Java 是一种自由形式的语言,额外的空格、制表符和回车并不会影响程序的生成结果。在 Java 中,你不能执行以下操作,即使这在 C 和 C++ 中是合法的: - -```JAVA -{ - int x = 12; - { - int x = 96; // Illegal - } -} -``` - -在上例中, Java 便编译器会在提示变量 x 已经被定义过了。因此,在 C 和 C++ 中可以于更大作用域中"隐藏"变量的能力在 Java 中是不被允许的。 因为 Java 的设计者认为这样的定义会混淆编程。 - - -### 对象作用域 - -Java 对象与基本类型具有不同的生命周期。当我们使用 `new` 关键字来创建 Java 对象时,它的生命周期将会超出作用域的末尾。因此,下面这段代码示例 - -```JAVA -{ - String s = new String("a string"); -} -// 作用域结束 -``` -上例中,变量 s 的范围在标注的地方结束了。但是,引用的字符串对象依然还在占用内存。在这段代码中,变量 s 的唯一引用超出了作用域,因此它无法在作用域外被访问。在后面的章节中,你我们还会学习怎么在编程中传递和复制对象的引用。 - -只要程序需要, `new` 出来的对象就不会被销毁。 相比在 C++ 编码中操作内存可能会出现的诸多问题,这些困扰在 Java 中都不复存在了。在 C++ 中你不仅要确保对象的内存在你操作的范围内存在,还必须在完成后销毁对象。 - -那么问题来了:我们在 Java 中并没有去主动清理这些对象,那么它是如何避免 C++ 中出现的内存泄漏和程序终止的问题呢?答案是:Java 的垃圾收集器会检查所有 `new` 出来的对象并判断哪些不再可达,继而释放那些被占用的内存。至此,我们不必再自己操作回收内存了。取而代之,只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的"内存泄漏"问题。 - - -## 类的创建 - -### 类型 - -如果一切都是对象,那么我们用什么来表示对象类的具体展现和行为呢?顾名思义,你可能很自然地想到 `type` 关键字。但是,事实上大多数面向对象的语言都使用 `class` 关键字类来描述一种新的对象。 通常在 `class` 关键字的后面的紧跟类的的名称。如下代码示例: - -```java - class ATypeName { - // 这里是类的内部 -} -``` - -在上例中,我们介绍了如何创建一个新的类型,尽管这个类里只有一行注释。但是我们一样可以通过 `new` 关键字来创建一个对象。如下: - -```JAVA -ATypeName a = new ATypeName(); -``` - -到现在为止,我们还不能用这个对象来做什么事,比如发送一些有趣的信息啊。除非我们在这个类里定义一些方法。 - - -### 属性 - -当我们创建好了一个类之后,我们可以往类里存放两种类型的元素。方法(**method**)和属性(**field**)。类的属性可以是基本类型。如果类的属性是对象的话,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都会为其属性保留独立的存储空间。通常,属性不再对象之间做共享。下面是一个包含部分属性的类的代码示例: - -```JAVA -class DataOnly { - int i; - double d; - boolean b; -} -``` - -除非持有数据,不然这个类不能做任何事。在此之前,我们可以通过下面的代码示例来创建它的对象: - -```JAVA - DataOnly data = new DataOnly(); -``` - -我们必须通过这个对象的引用来指定属性值。格式:对象名称.方法名称或成员名称。代码示例: - -```JAVA - data.i = 47; - data.d = 1.1; - data.b = false; -``` - -如果你想修改对象内部包含的另一个对象的数据,可以通过这样的格式修改。代码示例: - -```JAVA - myPlane.leftTank.capacity = 100; -``` - -你可以用这种方式嵌套许多对象(尽管这样的设计会带来混淆)。 - - - -### 基本类型默认值 - -如果类的成员变量(属性)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。 - -| 基本类型 | 初始值 | -| :-----: |:-----: | -| boolean | false | -| char | \u0000 | -| byte | 0 | -| short |0 | -| int | 0 | -| long | 0L | -| float | 0.0f | -| double | 0.0d | - -这些默认值仅在 Java 初始化类的时候才会被赋予。这种方式确保了基本类型的属性始终能被初始化(在C++ 中不会),从而减少了 bug 的来源。但是,这些初始值对于程序来说并不一定是合法或者正确的。 所以,为了安全,我们最好始终显式地初始化变量。 - -这种默认值的赋予并不适用于局部变量 —— 那些不属于类的属性的变量。 因此,若在方法中定义的基本类型数据,如下: - -```JAVA - int x; -``` - -这里的变量 x 不会自动初始化为0,因而在使用变量 x 之前,程序员有责任主动地为其赋值(和 C 、C++ 一致)。如果我们忘记了这一步,在 JAVA 中将会提示我们"编译时错误,该变量尚未被初始化"。 这一点做的比 C++ 要更好,在后者中,编译器只是提示警告,而在 JAVA 中则直接报错。 - - - -### 方法使用 - -在许多语言(如 C 和 C++)中,术语函数(**function**)用于描述命名子程序。在 Java 中,我们使用术语方法(**method**)来表示"做某事的方式"。 - -在 JAVA 中,方法决定着对象能接收哪些信息。方法的基础部分包含名称、参数、返回类型、方法体。格式如: - -```java - [返回类型][方法名](/*参数列表*/){ - // 方法体 - } -``` - -#### 返回类型 - -方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法的名称和参数列表被统称为**方法签名**(**signature of the method**)。签名作为方法的唯一性标识。 - -Java 中的方法只能作为类的一部分创建。它只能被对象所调用 [^4],并且该对象必须有权限来执行调用。若对象调用错误的方法,则程序将在编译时报错。 - -我们可以通过在对象名的后面跟上 `.` 符号+方法名及其参数来调用一个方法。代码示例: - -```JAVA -[对象引用].[方法名](参数1, 参数2, 参数3); -``` - -若方法不带参数,例如一个对象 a 的方法 f 不带参数并返回 int 型结果,我们可以如下表示。代码示例: - -```JAVA -int x = a.f(); -``` - -上例中方法 f 的返回值必须兼容接收的变量 x 。这种调用方法的行为有时被称为向对象传递信息。面向对象编程可以被总结为:向对象传递信息。 - - -#### 参数列表 - -方法参数列表指定传递给方法的信息。正如你可能猜到的,这些信息 —— 就像 Java 中的其他所有信息 —— 采用对象的形式。参数列表必须指定对象类型和每个对象的名称。同样,我们并没有直接处理对象,而是在传递对象引用 [^5] 。但是引用的类型必须是正确的。如果方法需要 String 参数,则必须传入 String,否则编译器将报错。 - -```JAVA -int storage(String s) { - - return s.length() * 2; -} -``` - -此方法计算并返回某个字符串的长度。参数 s 的类型为 String 。将 字符串变量 s 传递给 storage() 后,我们可以将其视为任何其他对象一样 —— 我们可以想起传递信息。在这里,我们调用 length() 方法,它是一个 String 方法,返回字符串长度。字符串中每个字符的大小为16位或两个字节。您还可以看到 **return** 关键字,它执行两项操作。首先,它意味着"方法执行结束"。其次,如果方法有返回值,那么该值就位于 **return** 语句之后。这里,返回值是通过计算 - -```JAVA -s.length() * 2 -``` -产生的。在方法中,我们可以返回任意的数据。如果我们不想方法返回什么数据,则可以通过给方法标识 `void` 来表明这是一个无需返回值的方法。 代码示例: - -```JAVA -boolean flag() { - return true; -} - -double naturalLogBase() { - return 2.718; -} - -void nothing() { - return; -} - -void nothing2() { - -} -``` - -当返回类型为 **void** 时, **return** 关键字仅用于退出方法,因此在方法结束处的 **return** 可被省略。我们可以随时从方法中返回。若方法返回类型为非 `void`,则编译器会强制返回相应类型的值。 - -上面的描述可能会让你感觉程序只不过是一堆包含各种方法的对象,将对象作为方法参数来传递信息给其他的对象。从表面上来看的确如此。但在下一章的运算符中我们将会学习如何在方法中做出决策来完成更底层、详细的工作。对于本章,知道如何传递信息就够了。 - - - -## 程序编写 - -在看到第一个 Java 程序之前,我们还必须了解其他几个问题。 - -#### 命名可见性 - -命名控制在任何一门编程语言中都是一个问题。如果程序员在两个模块中使用相同的命名,那么如何区分这两个名称,并防止两个名称发生"冲突"呢?在 C 语言编程中这是很具有挑战性的,因为程序通常是一个无法管理的名称海洋。C++ 将函数嵌套在类中,它们不能与嵌套在其他类中的函数名冲突。然而,C++ 继续允许全局数据和全局函数,因此仍有可能发生冲突。为了解决这个问题,C++ 使用附加的关键字引入命名空间。 - -Java 采取了一种新的方法避免了以上这些问题:为一个库生成一个明确的名称,Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。自从我的域名为 MindviewInc.com 开始,我就将我的 foibles 工具库命名为 com.mindviewinc.utility.foibles。根据你的域名的反向信息,`.`代表着一个子目录。 - -在 Java 1.0和 Java 1.1中,域扩展 com,edu,org,net 等按惯例大写,因此类库中会出现这样类似的名称:Com.mindviewinc.utility.foibles。然而,在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,该语言可以防止名称冲突。 - -使用反向 URL 是一种新的命名空间方法,在此之前尚未有其他语言这么做过。Java 中有许多这些"创造性"地解决问题的方法。正如你想象,如果我们未经测试就添加一个功能并用于生产,那么在将来发现该功能的问题再想纠正,通常为时已晚。(有些问题错误得足以从语言中删除。) - -使用反向 URL 将命名空间与文件路径相关联不会导致BUG,但它却给源代码管理带来麻烦。例如在 com.mindviewinc.utility.foiles 这样的目录结构中,我们创建了com、mindviewinc 空目录。它们存在的唯一目的就是用来表示这个反向的 URL。 - -这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。如果你希望以最初设计的方式使用目录,这种方法可以从"令人沮丧"到"令人抓狂",对于产生的 Java 代码,你基本上不得不使用专门为此设计的 IDE 来管理代码。例如 NetBeans,Eclipse 或 IntelliJ IDEA。实际上,这些 IDE 都为我们管理和创建深度空目录层次结构。 - -对于这本书中的例子,我不想让深层次的层次结构给你的学习带来额外的麻烦,这实际上需要你在开始之前学习熟悉一种重量级的 IDE。所以,我们的每个章节的示例都位于一个浅的子目录中,以章节标题为名。这导致我偶尔会与遵循深度层次方法的工具发生冲突。 - - -#### 使用其他组件 - - -无论何时在程序中使用预定义的类,编译器都必须找到该类。在一般情况下,该类已存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类未在文件中稍后定义(Java 消除了所谓的"前向引用"问题)。而那些存在于其他文件中的类怎么样?你可能认为编译器应该足够智能去找到它,但这样是有问题的。想象一下,假如你要使用某个类,但目录中存在多个同名的类(可能用途不同)。或者更糟糕的是,假设你正在编写程序,并且在构建它时,你将向库中添加一个与现有类名称冲突的新类。 - -要解决此问题,你必须通过使用 `import` 关键字来告诉 Java 编译器具体要使用的类。import 表示编译器引入一个包,它是一个类库。(在其他语言中,库可以包含函数和数据以及类,但请记住,Java 中的所有活动都在类中进行。)大多数时候,我们都在使用 JAVA 标准库中的组件。有了这些,你不用担心长的反向域名。你只用说,例如: - -```JAVA -import java.util.ArrayList; -``` - -上例可以告诉编译器使用位于标准库 util 下的 ArrayList 类。在 util 包含许多类,我们可以使用通配符 `*` 来导入其中部分类,无需显式导入。代码示例: - -```JAVA -import java.util.*; -``` - -本书中的示例很小,为简单起见,我们通常会使用 `.*` 形式略过导入。然而,许多教程书籍都会要求程序员单独导入每个类。 - - - -#### static关键字 - -类是对象的外观及行为方式的描述。通常只有在使用 `new` 关键字之后程序才能被分配存储空间以及使用其方法。这种方式在两种情况下是不足的。 - -1. 有时你只需要为特定字段分配一个共享存储空间,无论该类创建了多少个对象,或者即使没有创建任何对象; - -2. 创建一个与此类本身任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。 - -**static** 关键字(从 C++ 采用)就符合我们的要求。当我们说某些东西是静态的时,它意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问该字段或方法,因为非静态字段和方法必须与替对象关联 [^6] 。 - -一些面向对象的语言使用类数据(**class data**)和类方法(**class method**)这样的术语来表述静态。静态的数据意味着该数据和方法仅存在于类中,而非类的任何实例对象中。有时 Java 文献也使用这些术语。我们可以通过在类的属性或方法前添加 `static` 修饰来表示这是一个静态属性或静态方法。 代码示例: - -```JAVA -class StaticTest { - static int i = 47; -} -``` - -现在,即使你创建了两个 StaticTest 对象,但是静态变量 i 仍只占一份存储空间。两个对象都会共享相同的变量 i。 代码示例: - -```JAVA -StaticTest st1 = new StaticTest(); -StaticTest st2 = new StaticTest(); -``` - -st1.i 和 st2.i 的值都是47,因为它们属于同一段内存。引用静态变量有两种方法。在前面的示例中,我们可以通过一个对象来命名它;例如,st2.i。同时,你也可以通过它的类名直接调用它(这是非静态成员不能执行的操作): - -```JAVA -StaticTest.i ++; -``` - -`++` 运算符将会使变量结果 + 1。此时 st1.i 和 st2.i 的值就变成了48了。 - -使用类名直接引用静态变量的首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以使用特殊的附加语法 classname.method()来直接调用静态属性或方法 [^7]。 代码示例: - -```JAVA -class Incrementable { - static void increment() { - StaticTest.i++; - } -} -``` - -上例中 Incrementable 类调用静态方法 increment()。后者再使用 `++` 运算符递增静态变量 int i。我们依然可以先实例化对象再调用该方法。 代码示例: - -```JAVA -Incrementable sf = new Incrementable(); -sf.increment(); -``` - -当然了,首选的方法是直接通过类来调用它。代码示例: - -```JAVA -Incrementable.increment(); -``` - -相比非静态的对象,`static` 属性改变了创建数据的方式。同样,当 `static` 关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,`static` 关键字的这些特性对于应用程序入口点的 main() 方法尤为重要。 -应用于字段的 `static` 肯定会更改创建数据的方式 —— `static` 针对每个类和非 `static` 针对每个对象。当应用于方法时,`static` 允许您在不创建对象的情况下调用该方法。正如您将看到的,在定义作为运行应用程序入口点的main()方法时,这是非常重要的。 - - - -## 小试牛刀 - -最后,我们来开始编写第一个完整的程序。我们使用 Java 标准库来展示一个字符串和日期。 - -```JAVA - -// objects/HelloDate.java -import java.util.*; - -public class HelloDate { - public static void main(String[] args) { - System.out.println("Hello, it's: "); - System.out.println(new Date()); - } -} - -``` - -在这本书中,代码块的第一行,我将使用注释行,其中包含文件的路径信息(使用本章的目录名对象),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释信息轻松地在 Github 库中找到相应的代码示例。 - -如果你想在代码中使用到一些额外的库,那么你需要在程序文件的开始处使用 **import** 关键字来导入它们。之所以说是额外的,因为有一些库已经默认自动包含到每个文件里了。例如:**java.lang** 包。 - -现在打开你的浏览器在 [Oracle](https://www.oracle.com/) 上查看文档。如果你还没有在 [Oracle](https://www.oracle.com/) 网站上下载 JDK 文档,那就趁现在 [^8] 。查看包列表,你会看到 Java 附带的所有不同的类库。 - -选择 **java.lang**。这里显示的是该库中所有类的列表。由于 **java.lang** 隐式包含在每个 Java代码文件中,因此这些类是自动可用的。**java.lang** 中没有列出 **Date** 类,所以我们必须将其导入库才能使用它。如果你不清楚某个类名或者想查看所有的类,可以在 Java 文档中选择"Tree"。 - -现在,我们可以找到 Java 附带的每个类。使用浏览器的"查找"功能查找 "Date"。搜索结果中将会列出 java.util.Date,显而易见,它在 util 库中,所以我们必须导入 java.util.* 才能使用 Date。 - -如果你在文档中选择 java.lang,然后选择 System,你会看到 System 类中有几个字段,如果你选择 `out`,你会发现它是一个静态的 PrintStream 对象。 所以,即使我们没有使用 new 创建, `out` 对象就已经存在并可以使用。 `out` 对象可以执行的操作取决于 PrintStream 。 其在文档中的描述中显示为超链接,如果单击该链接,我们将可以看到 PrintStream 所对应的方法列表。(更多详情,将在本书后面介绍。) 现在我们重点说的是 println() 这个方法。 它的作用是 "将信息输出到控制台,并以换行符结束。" 既然如此,我们可以这样编码来输出信息到控制台。 代码示例: - -```JAVA - -System.out.println("A String of things"); -``` - -每个 java 源文件中允许有多个类。与此同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。每个独立的程序应该包含一个 main 方法作为程序运行的入口。其方法签名和返回类型如下。代码示例: - -```JAVA -public static void main(String[] args) { - -} -``` - -关键字 `public` 表示方法可以被外界访问到。( 更多详情将在 **隐藏实现** 章节讲到) -main() 方法的参数是一个 字符串(String) 数组。 参数 `args` 并没有在当前的程序中使用到,但是 Java 虚拟机强制要求必须要有。 这是因为它们被用于保存命令行中的参数。 - -下面我们来看一段有趣的代码: - -```JAVA -System.out.println(new Date()); -``` - -上面的示例中,我们创建了一个日期(Date)型对象并将其转化为字符串类型并输出到控制台中。 一旦这一行语句执行完毕,我们就不再需要该日期对象了。这时, JAVA 的垃圾回收器就可以将其占用的内存回收,我们无需去主动清除它们。 - -查看 JDK 文档,我们可以看到, **System** 类下还有很多其他有用的方法。( Java 的牛逼之处还在于,它拥有一个庞大的标准库资源。) 代码示例: - -```JAVA - -// objects/ShowProperties.java -public class ShowProperties { - public static void main(String[] args) { - System.getProperties().list(System.out); - System.out.println(System.getProperty("user.name")); - System.out.println(System.getProperty("java.library.path")); - } -} -``` - -输出结果(前20行): - -```text -java.runtime.name=Java(TM) SE Runtime Environment -sun.boot.library.path=C:\Program -Files\Java\jdk1.8.0_112\jr... -java.vm.version=25.112-b15 -java.vm.vendor=Oracle Corporation -java.vendor.url=http://java.oracle.com/ -path.separator=; -java.vm.name=Java HotSpot(TM) 64-Bit Server VM -file.encoding.pkg=sun.io -user.script= -user.country=US -sun.java.launcher=SUN_STANDARD -sun.os.patch.level= -java.vm.specification.name=Java Virtual Machine -Specification -user.dir=C:\Users\Bruce\Documents\GitHub\on-ja... -java.runtime.version=1.8.0_112-b15 -java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment -java.endorsed.dirs=C:\Program -Files\Java\jdk1.8.0_112\jr... -os.arch=amd64 -java.io.tmpdir=C:\Users\Bruce\AppData\Local\Temp\ -``` - -上例主方法中的第一行,会输出所有的系统属性,也就是环境信息。 **list()** 方法将结果发送给它的参数 **System.out** 在本书的后面,我们还会接触到将结果输出到其他地方,如文件中。另外,我们还可以请求特定的属性。该例中我们使用到了 **user.name** 和 **java.library.path**。 末尾的 "/* Output:"标记表示此文件生成的输出的开头。本书中产生输出的大多数示例将会以包含此注释形式的输出,因此我们可以看到输出并知道它是正确的。带有这个标签允许在使用编译器检查并执行后将输出自动更新到本书的文本中。 - - -### 编译和运行 - -要编译和运行本书中的代码示例,首先必须具有 Java 编程环境。 第二章的示例中描述了安装过程。如果你遵循这些说明,那么你将会在不受 Oracle 的限制的条件下用到 Java 开发人员工具包(JDK)。如果你使用其他开发系统,请查看该系统的文档以确定如何编译和运行程序。 第二章还介绍了如何安装本书的示例。 - -移动到 objects 子目录下并键入: - -``` bash -javac HelloDate.java -``` - -此命令不应生成任何响应。如果我们收到任何类型的错误消息,则表示未正确安装 JDK,并且必须检查这些问题。 - -此外,仅仅是执行的话,则可以键入: - -```JAVA -java HelloDate -``` -我们将会获得信息反馈,日期输出。这是我们编译和运行本书中每个程序(包含main())的过程 [^9]。但是,本书的源代码在根目录中也有一个名为 **build.gradle** 的文件,其中包含用于自动构建,测试和运行本书文件的 **Gradle** 配置。当您第一次运行 `gradlew` 命令时,**Gradle** 将自动安装(前提已安装Java)。 - - -## 编码风格 - - -Java 的代码约定规范(*Code Conventions for the Java Programming Language*)[^10] 要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如: - -```JAVA -class AllTheColorsOfTheRainbow { - // ``` -``` - - -这种风格也被称之为"驼峰式"。对于几乎所有其他方法,字段(成员变量)和对象引用名称 - 除了标识符的首字母是小写之外其他规范与类的命名一致。代码示例: - -```JAVA -class AllTheColorsOfTheRainbow { - int anIntegerRepresentingColors; - void changeTheHueOfTheColor(int newHue) { - // ... - } - // ... -} - -``` - -在 Oracle 的库中,开放式花括号的位置同样遵循和本书中相同的规范。 - - -## 本章小结 - - -本章向您展示了简单的 Java 程序编写以及该语言相关的基本概念。到目前为止,我们的示例都只是些简单的顺序执行。在接下来的两章里,我们将会接触到 Java 的一些基本操作符,以及如何去控制程序执行的流程。 - - - [^1]: 这里可能有争议。有人说这是一个指针,但这假定了一个潜在的实现。此外,Java 引用的语法更类似于 C++ 引用而非指针。在 *Thinking in Java* 的第 1 版中,我发明了一个新术语叫"句柄"(*handle*),因为 C++ 引用和Java 引用有一些重要的区别。作为一个从 C++ 的过来人,我不想混淆 Java 可能的最大受众 —— C++ 程序员。在*Thinking in Java* 的第 2 版中,我认为"引用"(*reference*)是更常用的术语,从 C++ 转过来的人除了引用的术语之外,还有很多东西需要处理,所以他们不妨双脚都跳进去。但是,也有些人甚至不同意"引用"。在某书中我读到一个观点:Java 支持引用传递的说法是完全错误的,因为 Java 对象标识符(根据该作者)实际上是"对象引用"(*object references*),并且一切都是值传递。所以你不是通过引用传递,而是"通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。) - - [^2]: 大多数微处理器芯片都有额外的高速缓冲存储器,但这是按照传统存储器而不是寄存器。 - - [^3]: 一个例子是字符串常量池。所有文字字符串和字符串值常量表达式都会自动放入特殊的静态存储中。 - - [^4]: 静态方法,我们很快就能接触到,它可以在没有对象的情况下直接被类调用。 - - [^5]: 通常除了前面提到的"特殊"数据类型 boolean,char,byte,short,int,long,float 和 double。通常来说,传递对象就意味者传递对象的引用。 - - [^6]: 静态方法在使用之前不需要创建对象,因此它们不能直接调用非静态的成员或方法(因为非静态成员和方法必须要先实例化为对象才可以被使用)。 - - [^7]: 在某些情况下,它还为编译器提供了更好的优化可能。 - - [^8]: 请注意,此文档未包含在 JDK 中;你必须单独下载才能获得它。 - - [^9]: 对于本书中编译和运行命令行的每个程序,你可能还需要设置 CLASSPATH 。 - - [^10]: 为了保持本书的代码排版紧凑,我并没完全遵守规范,但我尽量会做到符合 Java 标准。 - - \ No newline at end of file diff --git a/book/04-Operators.md b/book/04-Operators.md deleted file mode 100644 index e03f35e2..00000000 --- a/book/04-Operators.md +++ /dev/null @@ -1,406 +0,0 @@ -[TOC] - - -# 第四章 运算符 - - ->运算符操纵数据。 - -Java 是从 C++ 的基础上做了一些改进和简化发展而成的。对于 C/C++ 程序员来说,Java 的运算符并不陌生。如果你已了解 C 或 C++,大可以跳过本章和下一章,直接阅读 Java 与 C/C++ 不同的地方。 - -如果理解这两章的内容对你来说还有点困难,那么我推荐你先了解下 *Thinking in C* 再继续后面的学习。 这本书现在可以在 [www.OnJava8.com](http://www.OnJava8.com]) 上免费下载。它的内容包含音频讲座、幻灯片、练习和解决方案,专门用于帮助你快速掌握学习 Java 所需的基础知识。 - - - -## 使用说明 - - -运算符接受一个或多个参数并生成新值。这个参数与普通方法调用的形式是不同的,但效果是相同的。加法 `+`,减法 `-`,乘法 `*`,除法 `/` 以及赋值 `=` 在任何编程语言中的工作方式都是类似的。所有运算符都能根据自己的运算对象生成一个值。除此以外,一个运算符可改变运算对象的值,这叫作"副作用"(**Side Effect**)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。 - -几乎所有运算符都只能操作基本类型(*Primitives*)。唯一的例外是 `=`、`==` 和 `!=`,它们能操作所有对象(这也是令人混淆的一个地方)。除此以外,**String** 类支持 `+` 和 `+=`。 - - - -## 优先级 - - -运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。Java 对计算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定计算顺序。代码示例: - -```JAVA -// operators/Precedence.java -public class Precedence { - - public static void main(String[] args) { - int x = 1, y = 2, z = 3; - int a = x + y - 2/2 + z; // [1] - int b = x + (y - 2)/(2 + z); // [2] - System.out.println("a = " + a); - System.out.println("b = " + b); - } -} -``` - - 输出结果: - -``` - a = 5 - b = 1 -``` - -这些语句看起来大致相同,但从输出中我们可以看出它们具有非常不同的含义,具体取决于括号的使用。 - -我们注意到,在 `System.out.println()` 语句中使用了 `+` 运算符。 但是在这里 `+` 代表的意思是字符串连接符。编译器会将 `+` 连接的非字符串尝试转换为字符串。上例中的输出结果说明了 a 和 b 都已经被转化成了字符串。 - - - -## 赋值 - - -运算符的赋值是由符号 `=` 完成的。它代表着获取 `=` 右边的值并赋给左边的变量。右边可以是任何常量、变量或者是可产生一个返回值的表达式。但左边必须是一个明确的、已命名的变量。也就是说,必须要有一个物理的空间来存放右边的值。举个例子来说,可将一个常数赋给一个变量( A = 4 ),但不可将任何东西赋给一个常数(比如不能 4 = A)。 - -基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。举个例子,a = b ,如果 b 是基本类型,那么 赋值操作会将 b 的值复制一根给变量 a, 此后若 a 的值发生改变是不会影响到 b 的。作为一名程序员,着应该成为我们的常识。 - -如果是为对象赋值,那么结果就不一样了。对一个对象进行操作时,我们实际上操作的是它的引用。所以我们将右边的对象赋予给左边时,赋予的只是该对象的引用。此时,两者指向的堆中的对象还是同一个。代码示例: - -```JAVA -// operators/Assignment.java -// Assignment with objects is a bit tricky -class Tank { - int level; -} - -public class Assignment { - - public static void main(String[] args) { - Tank t1 = new Tank(); - Tank t2 = new Tank(); - t1.level = 9; - t2.level = 47; - System.out.println("1: t1.level: " + t1.level + - ", t2.level: " + t2.level); - t1 = t2; - System.out.println("2: t1.level: " + t1.level + - ", t2.level: " + t2.level); - t1.level = 27; - System.out.println("3: t1.level: " + t1.level + - ", t2.level: " + t2.level); - } -} -``` - -输出结果: - -``` -1: t1.level: 9, t2.level: 47 -2: t1.level: 47, t2.level: 47 -3: t1.level: 27, t2.level: 27 -``` - -这是个简单的 `Tank` 类,通过 main 方法 `new` 出了 2 个实例对象。 两个对象的 `level` 属性分别被赋予了不同的值。 然后,t2 的值被赋予给 t1。在许多编程语言里,预期的结果是 t1 和 t2 的值会一直相对独立。但是,在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时被丢失,它就将会在垃圾回收时被清理)。 - -这种现象通常称为别名(*aliasing*),这是 Java 处理对象的一种基本方式。但是假若你不想这里出现这样的混淆的话,你可以这么做。代码示例: - -```JAVA -t1.level = t2.level; -``` - -较之前的做法,这样做保留了两个单独的对象,而不是丢弃一个并将 t1 和 t2 绑定到同一个对象。但是这样的操作有点违背 JAVA 的设计原则。对象的赋值是个需要重视的环节,否则你可能收获意外的"惊喜"。 - - -### 方法调用中的别名现象 - -当我们把对象传递给方法时,会发生别名现象。 - -```JAVA -// operators/PassObject.java -// 正在传递的对象可能不是你之前使用的 -class Letter { - char c; - } - -public class PassObject { - static void f(Letter y) { - y.c = 'z'; - } - - public static void main(String[] args) { - Letter x = new Letter(); - x.c = 'a'; - System.out.println("1: x.c: " + x.c); - f(x); - System.out.println("2: x.c: " + x.c); - } -} -``` - -输出结果: - -``` -1: x.c: a -2: x.c: z -``` - -在许多编程语言中,方法 **f()** 似乎在内部复制其参数 **Letter y**。但是一旦传递了一个引用,那么实际上 `y.c ='z';` 是在方法 **f()** 之外改变对象。别名现象以及其解决方案是个复杂的问题,在附录中有包含:[对象传递和返回](./Appendix-Passing-and-Returning-Objects.md)。意识到这一点,我们可以警惕类似的陷阱。 - - - -## 算术运算符 - -Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 `+`、减号 `-`、除号 `/`、乘号 `*` 以及模数`%`(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。 - -Java 也用一种简写形式同时进行运算和赋值操作,由运算符后跟等号表示,并且与语言中的所有运算符一致(只要有意义)。为了将 4 的值赋予给变量 x 同时将结果赋予给 x , 可用 x += 4 来表示。下面带来代码更多代码示例: - - -```JAVA -// operators/MathOps.java -// The mathematical operators -import java.util.*; - -public class MathOps { - public static void main(String[] args) { - // Create a seeded random number generator: - Random rand = new Random(47); - int i, j, k; - // Choose value from 1 to 100: - j = rand.nextInt(100) + 1; - System.out.println("j : " + j); - k = rand.nextInt(100) + 1; - System.out.println("k : " + k); - i = j + k; - System.out.println("j + k : " + i); - i = j - k; - System.out.println("j - k : " + i); - i = k / j; - System.out.println("k / j : " + i); - i = k * j; - System.out.println("k * j : " + i); - i = k % j; - System.out.println("k % j : " + i); - j %= k; - System.out.println("j %= k : " + j); - // 浮点运算测试 - float u, v, w; // Applies to doubles, too - v = rand.nextFloat(); - System.out.println("v : " + v); - w = rand.nextFloat(); - System.out.println("w : " + w); - u = v + w; - System.out.println("v + w : " + u); - u = v - w; - System.out.println("v - w : " + u); - u = v * w; - System.out.println("v * w : " + u); - u = v / w; - System.out.println("v / w : " + u); - // 下面的操作同样适用于 char, - // byte, short, int, long, and double: - u += v; - System.out.println("u += v : " + u); - u -= v; - System.out.println("u -= v : " + u); - u *= v; - System.out.println("u *= v : " + u); - u /= v; - System.out.println("u /= v : " + u); - } -} - -``` - -输出结果: - -``` -j : 59 -k : 56 -j + k : 115 -j - k : 3 -k / j : 0 -k * j : 3304 -k % j : 56 -j %= k : 3 -v : 0.5309454 -w : 0.0534122 -v + w : 0.5843576 -v - w : 0.47753322 -v * w : 0.028358962 -v / w : 9.940527 -u += v : 10.471473 -u -= v : 9.940527 -u *= v : 5.2778773 -u /= v : 9.940527 -``` - -为了生成随机数字,程序首先创建一个 **Random** 对象。不带参数的 **Random** 对象会利用当前的时间用作随机数生成器的"种子"(*seed*),从而为程序的每次执行生成不同的输出。在本书的示例中,重要的是每个示例末尾的输出尽可能一致,以便可以使用外部工具进行验证。所以我们通过在创建 **Random** 对象时提供种子(随机数生成器的初始化值,其始终为特定种子值产生相同的序列),让程序每次执行都生成相同的随机数,如此以来输出结果就是可验证的 [^1]。 若需要生成随机值,可删除代码示例中的种子参数。该对象通过调用方法 **nextInt()** 和 **nextFloat()**(还可以调用 **nextLong()** 或 **nextDouble()**),使用 **Random** 对象生成许多不同类型的随机数。**nextInt()** 的参数设置生成的数字的上限,下限为零,为了避免零除的可能性,结果偏移1。 - - - -### 一元加减运算符 - -一元加 `+` 减 `-` 运算符的操作和二元是相同的。编译器可自动识别使用何种方式解析运算: - -```JAVA -x = -a; -``` - -上例的代码表意清晰,编译器可正确识别。下面再看一个示例: - -```JAVA -x = a * -b; -``` - -虽然编译器可以正确的识别,但是程序员可能会迷惑。为了避免混淆,推荐下面的写法: - -```JAVA -x = a * (-b); -``` - -一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为了 **int** 类型。 - - - -## 自动递增和递减 - - -和 C 语言类似,Java 提供了许多快捷运算方式。快捷运算可使代码可读性,可写性都更强。其中包括递增 `++` 和递减 `--`,意为"增加或减少一个单位"。举个例子来说,假设 a 是一个 **int** 类型的值,则表达式 `++a` 就等价于 `a = a + 1`。 递增和递减运算符不仅可以修改变量,还可以生成变量的值。 - -每种类型的运算符,都有两个版本可供选用;通常将其称为"前缀版"和"后缀版"。"前递增"表示 `++` 运算符位于变量或表达式的前面;而"后递增"表示 `++` 运算符位于变量或表达式的后面。类似地,"前递减"意味着 `--` 运算符位于变量或表达式的前面;而"后递减"意味着 `--` 运算符位于变量或表达式的后面。对于前递增和前递减(如 `++a` 或 `--a`),会先执行递增/减运算,再返回值。而对于后递增和后递减(如 `a++` 或 `a--`),会先返回值,再执行递增/减运算。代码示例: - -```JAVA -// operators/AutoInc.java -// 演示 ++ 和 -- 运算符 -public class AutoInc { - public static void main(String[] args) { - int i = 1; - System.out.println("i: " + i); - System.out.println("++i: " + ++i); // 前递增 - System.out.println("i++: " + i++); // 后递增 - System.out.println("i: " + i); - System.out.println("--i: " + --i); // 前递减 - System.out.println("i--: " + i--); // 后递减 - System.out.println("i: " + i); - } -} -``` - -输出结果: - -``` -i: 1 -++i: 2 -i++: 2 -i: 3 ---i: 2 -i--: 2 -i: 1 -``` - -对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有"副作用"的运算符(除那些涉及赋值的以外) —— 它们既改变操作数又改变值。 - -C++ 名称来自于递增运算符,同时也代表着"比 C 更进一步"。在早期的 Java 演讲中,*Bill Joy*( Java 创建者之一)说"**Java = C ++ --**"(C++ 减减)。这意味着 Java 是在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。 - - - -## 关系运算符 - - -关系运算符会产生一个布尔(**boolean**)结果,指示操作数值之间的关系。如果关系为真,则关系表达式生成 **true**,如果关系非真,则生成 **false**。关系运算符包括小于 `<`,大于 `>`,小于或等于 `<=`,大于或等于 `>=`,等价 `==` 和不等价 `!=`。`==` 和 `!=` 可与所有基本类型搭配使用。但其他类型的比较就不太适合了,因为布尔值只能是 **true** 或 **false**,所以比较他们之间的"大于"或"小于"没有意义。 - - -### 测试对象等价 - -关系运算符 `==` 和 `=` 也适用于所有对象,但它们的含义常常会混淆初次使用 Java 的程序员。代码示例: - -```JAVA -// operators/Equivalence.java -public class Equivalence { - public static void main(String[] args) { - Integer n1 = 47; - Integer n2 = 47; - System.out.println(n1 == n2); - System.out.println(n1 != n2); - } -} -``` - -输出结果: - -``` -true -false -``` - -看起来结果是我们所期望的。但其实事情不是那么简单。下面我们来创建自己的类: - -```JAVA -// operators/EqualsMethod2.java -// 默认的 equals() 方法没有比较内容 -class Value { -int i; -} - -public class EqualsMethod2 { - public static void main(String[] args) { - Value v1 = new Value(); - Value v2 = new Value(); - v1.i = v2.i = 100; - System.out.println(v1.equals(v2)); - } -} -``` - -输出结果: - -``` -false -``` - -现在事情再次令人困惑:结果是错误的。这是因为 **equals()** 的默认行为是比较对象的引用。因此,除非你在新类中重写 **equals()** 方法,否则我们将获取不到想要的结果。不幸的是,在学习 [复用](./08-Reuse.md)(**Reuse**) 章节后我们才能接触到"覆盖"(**override**),并且直到 [附录:集合主题](./Appendix-Collection-Topics.md),才能知道定义 **equals()** 方法的正确方式,但是现在明白 **equals()** 行为方式也可能为你节省一些时间。 - -大多数 Java 库类通过覆写 **equals()** 方法比较对象的内容而不是其引用。 - - -## 逻辑运算符 - - - -## 字面值 - - - -## 按位运算符 - - - -## 移位运算符 - - - -## 三元运算符 - - - -## 字符串运算符 - - - -## 常见陷阱 - - - -## 类型转换 - - - -## Java没有sizeof - - - -## 运算符总结 - - - -## 本章小结 - - - - diff --git a/book/05-Control-Flow.md b/book/05-Control-Flow.md deleted file mode 100644 index 10fac117..00000000 --- a/book/05-Control-Flow.md +++ /dev/null @@ -1,789 +0,0 @@ -[TOC] - -# 第五章 控制流 - -> 程序操纵它的世界并做出选择。 在Java中,您可以使用执行控制语句进行选择。 - -Java 使用 C 的所有执行控制语句,因此如果您使用 C 或 C++ 进行编程,那么您所看到的大部分内容都是熟悉的。 大多数过程编程语言都有某种控制语句,并且语言之间经常存在重叠。在 Java 中,关键字包括 **if-else,while,do-while,for,return,break** 和名为 **switch** 的选择语句。 但是,Java 并不支持备受诟病的 **goto**(它仍然是解决某些类型问题的最有效方法)。 你仍然可以进行类似goto的跳转,但它比其他语言中的 **goto** 更受限制。 - -[TOC] - -## true和flase - -条件表达式的示例是 **a == b**。这里有一个条件表达式示例,它使用条件操作符 **==** 来查看 **a** 的值是否等于 **b** 的值。 表达式返回 **true** 或 **false**。 如果显示条件表达式的结果,则会生成表示布尔值的字符串 **true** 和 **false**。 - -```java -// control/TrueFalse.java -public class TrueFalse { - public static void main(String[] args) { - System.out.println(1 == 1); - System.out.println(1 == 2); - } -} -/* Output: -true false */ -``` - -您在上一章中看到的任何关系运算符都可以生成条件语句。 请注意,Java 不允许您使用数字作为布尔值,即使它在 C 和 C++ 中允许(在这些语言中,"真"为非零,而"假"是零)。如果想在布尔测试中使用非布尔值,例如**if (a)**,必须首先使用一个条件表达式将其转换为布尔值,例如 **if (a!= 0)**。 - -## if-else - -**if-else** 语句是控制程序流程的最基本的形式。 其中的 **else** 是可选的,因此您可以使用两种形式的 **if**: - -```java -if(Boolean-expression) - statement -``` - -或 - -```java -if(Boolean-expression) - statement -else - statement -``` - -布尔表达式必须生成布尔结果,statement 指用分号结尾的简单语句,或复合语句——封闭在花括号内的一组简单语句。 每当使用 "statement" 一词时,它总是暗示语句可以是简单的或复合的。 - -作为 **if-else** 的一个例子,下面这个 **test()** 方法可以告诉您,您猜的数大于、小于还是等于目标数: - -```java -// control/IfElse.java -public class IfElse { - static int result = 0; - static void test(int testval, int target) { - if(testval> target) - result = +1; - else if(testval < target) // [1] - result = -1; - else - result = 0; // Match - } - public static void main(String[] args) { - test(10, 5); - System.out.println(result); - test(5, 10); - System.out.println(result); - test(5, 5); - System.out.println(result); - } -} -/* Output: -1 --1 -0 -*/ -``` - -**[1]** "else if" 不是新关键字,只是一个 else 后跟一个新的 if 语句。 - -尽管 Java(如之前的 C 和 C++)是一种"自由格式"语言,但通常会缩进控件流语句的主体,以便读者可以轻松确定它的开始和结束位置。 - - - -## 迭代语句 - -循环由 **while**,**do-while** 和 **for** 控制,有时称为迭代语句。 语句会重复执行,直到起控制作用的 **布尔表达式** (Boolean expression)计算为"假"。 - - -### while - -while循环的形式是: - -```java -while(Boolean-expression) - statement -``` - -在循环开始时,会计算一次布尔表达式的值,而在语句的每次进一步迭代之前再次计算一次。 - -下面这个简单的例子将可产生随机数,直到满足特定条件: - -```java -// control/WhileTest.java - -public class WhileTest { - static boolean condition() { - boolean result = Math.random() < 0.99; - System.out.print(result + ", "); - return result; - } - public static void main(String[] args) { - while(condition()) - System.out.println("Inside 'while'"); - System.out.println("Exited 'while'"); - } -} -/* Output: (First and Last 5 Lines) -true, Inside 'while' -true, Inside 'while' -true, Inside 'while' -true, Inside 'while' -true, Inside 'while' -...________...________...________...________... -true, Inside 'while' -true, Inside 'while' -true, Inside 'while' -true, Inside 'while' -false, Exited 'while' -*/ -``` - -**condition()** 方法用到了 **Math** 库里的 **static** (静态)方法 **random()**,该方法的作用是产生 0 和 1 之间 (包括 0,但不包括 1) 的一个**double**值。**result** 的值是通过比较操作符<而得到它的,这个操作符产生 **boolean** 类型的结果。在打印 **boolean** 类型的值时,将自动地得到适合的字符串 **true** 或 **false**。while 的条件表达式意思是说:"只要 **condition()**返回 **true**,就重复执行循环体中的语句"。 - -### do-while - -**do-while** 的格式如下: - -```java -do - statement -while(Boolean-expression); -``` - -**while** 和 **do-while** 之间的唯一区别是 **do-while** 的语句总是至少执行一次,即使表达式第一次计算为 false 也是如此。 在 **while** 循环结构中,如果条件第一次就为 **false**,那么其中的语句根本不会执行。在实际应用中,**while** 比 **do-while** 更常用一点。 - -### for - -**for** 循环可能是最常用的迭代形式。 该循环在第一次迭代之前执行初始化。 随后,它会执行条件测试,并在每次迭代结束时,进行某种形式的"步进"。**for** 循环的形式是: - -```java -for(initialization; Boolean-expression; step) - statement -``` - -初始化 (initialization) 表达式、布尔表达式 (Boolean-expression) ,或者步进 (step) 运算,都可以为空。在每次迭代之前都会测试表达式,并且一旦计算结果为 false,就会在 for 语句后面的行继续执行。 在每个循环结束时,执行一次步进。 - -**for**循环通常用于"计数"任务: - -```java -// control/ListCharacters.java - -public class ListCharacters { - public static void main(String[] args) { - for(char c = 0; c < 128; c++) - if(Character.isLowerCase(c)) - System.out.println("value: " + (int)c + - " character: " + c); - } -} -/* Output: (First 10 Lines) -value: 97 character: a -value: 98 character: b -value: 99 character: c -value: 100 character: d -value: 101 character: e -value: 102 character: f -value: 103 character: g -value: 104 character: h -value: 105 character: i -value: 106 character: j -... -*/ -``` - -请注意,变量 **c** 在程序用到它的地方被定义的,也就是在for循环的控制表达式内,而不是在 **main()** 的开头。**c** 的范围是由 **for** 控制的语句。 - -像C这样的传统过程语言要求在块的开头定义所有变量。 当编译器创建块时,它可以为这些变量分配空间。 在Java和C ++中,您可以在整个块中传播变量声明,并在需要时定义它们。 这允许更自然的编码风格并使代码更容易理解。[1] - -这个程序也使用了 **java.lang.Character** 包装器类,这个类不但能把 **char** 基本类型的值包装进对象,还提供了一些别的有用的方法。这里用到了 **static isLowerCase()** 方法来检查问题中的字符是否为小写字母。 - - - -#### 逗号操作符 - -逗号运算符(不是逗号分隔符,逗号作为分隔符用于分隔定义和方法参数)在 Java 中只有一个用法:在**for** 循环的控制表达式中。 在控制表达式的初始化和步进控制中,您可以使用逗号分隔多个语句,并按顺序计算这些语句。使用逗号运算符,您可以在 **for** 语句中定义多个变量,但它们必须属于同一类型: - -```java -// control/CommaOperator.java - -public class CommaOperator { - public static void main(String[] args) { - for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) { - System.out.println("i = " + i + " j = " + j); - } - } -} -/* Output: -i = 1 j = 11 -i = 2 j = 4 -i = 3 j = 6 -i = 4 j = 8 -*/ -``` - -**for** 语句中的 **int** 定义覆盖了 **i** 和 **j**,在初始化部分实际上可以拥有任意数量的具有相同类型的变量定义。在控制表达式中定义变量的能力仅限于 **for** 循环。 您不能将此方法与任何其他选择或迭代语句一起使用。 - -可以看到,无论在初始化还是在步进部分,语句都是顺序执行的。 - -## for-in语法 - -Java 5引入了更简洁的 **for** 语法,用于数组和集合(您将在"数组和集合"章节中了解更多有关这些内容的信息)。 这有时被称为 **增强版for循环**,并且您将看到的大部分文档称为 *for-each* 语法,但Java 8添加了大量使用的 **forEach()**。 这会混淆术语,因此我称之为*for-in* (例如,在Python中,你实际上是 **for x in sequence**,所以有合理的先例)。 请记住,您可能会在其他地方看到它的不同叫法。 - -**for-in** 会自动为您生成每个项,因此你不必创建 **int** 变量去对由访问项构成的序列进行计数。 例如,假设您有一个 **float** 数组,并且您想要选取该数组中的每个元素: - -```java -// control/ForInFloat.java - -import java.util.*; - -public class ForInFloat { - public static void main(String[] args) { - Random rand = new Random(47); - float[] f = new float[10]; - for(int i = 0; i < 10; i++) - f[i] = rand.nextFloat(); - for(float x : f) - System.out.println(x); - } -} -/* Output: -0.72711575 -0.39982635 -0.5309454 -0.0534122 -0.16020656 -0.57799757 -0.18847865 -0.4170137 -0.51660204 -0.73734957 -*/ -``` - -这个数组是用旧式的for循环组装的,因为在组装时必须按索引访问它。在下面这行中可以看到foreach方法: - -```java -for(float x : f) { -``` - -这条语句定义了一个 **float** 类型的变量 **x**,继而将每一个 **f** 的元素赋值给 **x**。 - -任何返回一个数组的方法都可以使用 **for-in**。例如 **String** 类有一个方法 **toCharArray()**,它返回一个 **char** 数组,因此可以很容易地下像下面这样迭代在字符串里面的所有字符: - -```java -// control/ForInString.java - -public class ForInString { - public static void main(String[] args) { - for(char c : "An African Swallow".toCharArray()) - System.out.print(c + " "); - } -} -/* Output: -A n A f r i c a n S w a l l o w -*/ -``` - -就像在集合的章节所看到的,for-in还可以用于任何 **iterable** 对象。 - -许多 **for** 语句都会在一个整型值序列中步进,就像下面这样: - -```java -for(int i = 0; i < 100; i++) -``` - -对于这样的语句,for-in语法将不起作用,除非先创建一个 **int** 数组。为了简化这些任务,我会在**onjava.Range** 包中创建一个名为 **range()** 的方法,它将自动生成恰当的数组。 - -隐藏实施过程这一章节( Implementation Hiding )介绍了静态导入。 但是,您无需了解这些详细信息即可开始使用此库。 你可以在 **import** 语句中看到 **static import** 语法: - -```java -// control/ForInInt.java - -import static onjava.Range.*; - -public class ForInInt { - public static void main(String[] args) { - for(int i : range(10)) // 0..9 - System.out.print(i + " "); - System.out.println(); - for(int i : range(5, 10)) // 5..9 - System.out.print(i + " "); - System.out.println(); - for(int i : range(5, 20, 3)) // 5..20 step 3 - System.out.print(i + " "); - System.out.println(); - for(int i : range(20, 5, -3)) // Count down - System.out.print(i + " "); - System.out.println(); - } -} -/* Output: -0 1 2 3 4 5 6 7 8 9 -5 6 7 8 9 -5 8 11 14 17 -20 17 14 11 8 -*/ -``` - -**range()** 方法已经被重载,重载表示相同的方法名可以具有不同的参数列表(你将很快学习重载)。**range()** 的第一个种重载形式是从0开始产生值,直到范围的上限,但不包括该上限。第二种重载形式是从第一个值开始产生值,直至比第二值小1的值为止。第三种形式有一个步进值,因此它每次的增量为该值。第四种 **range()** 表明还可以递减。**range()**是所谓生成器的一个非常简单的版本,有关生成器的内容将在本书稍后进行介绍。 - -**range()** 允许在更多地方使用 for-in 语法,因此可以说提高可读性。 - -请注意,**System.out.print()** 不会输出换行符,因此您可以分段输出一行。 - -*for-in* 语法不仅可以节省编写代码的时间。 更重要的是,它更容易阅读并说明你要做什么(获取数组的每个元素)而不是详细说明你是如何做到的("我正在创建这个索引,所以我可以使用它来选择每个数组元素。")。 本书只要有可能就会使用 *for-in* 语法。 - -## return - -在Java中有几个关键字代表无条件分支,这意味着分支在没有任何测试的情况下发生。 这些包括**return**,**break**,**continue** 和跳转到带标签语句的方法,类似于其他语言中的 **goto**。 - -**return**关键字有两方面的用途:一方面指定一个方法返回什么值(假设它没有 **void** 返回值),另一方面它会导致当前的方法退出,并返回那个值。可据此改写上面的 **IfElse.java** 里的 **test()** 方法,使其利用这些特点: - -```java -// control/TestWithReturn.java - -public class TestWithReturn { - static int test(int testval, int target) { - if(testval> target) - return +1; - if(testval < target) - return -1; - return 0; // Match - } - public static void main(String[] args) { - System.out.println(test(10, 5)); - System.out.println(test(5, 10)); - System.out.println(test(5, 5)); - } -} -/* Output: -1 --1 -0 -*/ -``` - -不需要 **else**,因为该方法在执行返回后不会继续执行。 - -如果在返回 **void** 的方法中没有 **return** 语句,则在该方法结束时会有一个隐式的 **return**,因此一个方法中并不总是需要包含 **return** 语句。 但是,如果您的方法声明它将返回除 **void** 之外的任何返回类型,则必须确保每个代码路径都返回一个值。 - -## break和continue - -在任何迭代语句的主体部分,都可以使用 **break** 和 **continue** 来控制循环的流程。 其中,**break** 退出循环而不执行循环中的其余语句。 而 **continue** 停止执行当前的迭代,然后退回循环起始处,以开始下一次迭代。 - -下面这个程序向大家展示了 **break** 和 **continue** 在 **for** 和 **while** 循环中的例子: - -```java -// control/BreakAndContinue.java -// Break and continue keywords - -import static onjava.Range.*; - -public class BreakAndContinue { - public static void main(String[] args) { - for(int i = 0; i < 100; i++) { // [1] - if(i == 74) break; // Out of for loop - if(i % 9 != 0) continue; // Next iteration - System.out.print(i + " "); - } - System.out.println(); - // Using for-in: - for(int i : range(100)) { // [2] - if(i == 74) break; // Out of for loop - if(i % 9 != 0) continue; // Next iteration - System.out.print(i + " "); - } - System.out.println(); - int i = 0; - // An "infinite loop": - while(true) { // [3] - i++; - int j = i * 27; - if(j == 1269) break; // Out of loop - if(i % 10 != 0) continue; // Top of loop - System.out.print(i + " "); - } - } -} -/* Output: -0 9 18 27 36 45 54 63 72 -0 9 18 27 36 45 54 63 72 -10 20 30 40 -*/ -``` - -**[1]** 在这个 **for** 循环中,**i** 的值永远不会达到 100;因为一旦 **i** 达到 74,**break** 语句就会中断循环。通常,只有在不知道中断条件何时满足时,才需要这样使用 **break**。因为**i** 不能被 9 整除,**continue** 语句就会使执行过程返回到循环的最开头(这使 **i** 递增)。如果能够整除,则将值显示出来。 - -**[2]** 使用 for-in 语法将产生相同的结果。 - -**[3]** 最后,可以看到一个"无穷 **while** 循环"的情况。然而,循环内部有一个 **break** 语句,可中止循环。请注意,**continue** 语句将控制权移回循环的顶部,而不会执行 **continue** 之后的任何操作。 因此,只有当 i 的值可被 10 整除时才会显示。在输出中,显示值 0,因为 0%9 产生0。 - -无限循环的另一种形式是 **for(;;)**。 编译器以同样的方式处理 **while(true)** 和 **for(;;)**,因此使用哪种取决于编程品味。 - - - -## 臭名昭著的goto - -goto 关键字很早就在程序设计语言中出现。事实上,goto 是汇编语言的程序控制结构的始祖:"若条件A,则跳到这里;否则跳到那里"。若阅读由几乎所有编译器生成的汇编代码,就会发现程序控制里包含了许多跳转。然而,goto是在源码的级别跳转的,所以招致了不好的声誉。若程序总是从一个地方跳到另一个地方,还有什么办法能识别代码的流程呢?随着 Edsger Dijkstra 著名的 "Goto 有害" 论的问世,goto 便从此失宠。 - -事实上,真正的问题并不在于使用 goto,而在于 goto 的滥用。而且在一些少见的情况下,goto 是组织控制流程的最佳手段。 - -尽管 goto 仍是 Java 的一个保留字,但并未在语言中得到正式使用;Java 没有 goto。然而,在 break和 continue 这两个关键字的身上,我们仍然能看出一些goto的影子。它并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 goto 问题中一起讨论,是由于它们使用了相同的机制:标签。 - -"标签"是后面跟一个冒号的标识符,就像下面这样: - -```java -label1: -``` - -对 Java 来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方——在标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环或者一个开关。这是由于 break 和 continue 关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方。如下所示: - -```java -label1: -outer-iteration { - inner-iteration { - // ... - break; // [1] - // ... - continue; // [2] - // ... - continue label1; // [3] - // ... - break label1; // [4] - } -} -``` - -**[1]** **break** 中断内部循环,并在外部循环结束。 - -**[2]** **continue** 移回内部循环的起始处。但在条件3中,**continue label1** 却同时中断内部循环以及外部循环,并移至 **label1** 处。 - -**[3]** 随后,它实际是继续循环,但却从外部循环开始。 - -**[4]** **break label1** 也会中断所有循环,并回到 **label1** 处,但并不重新进入循环。也就是说,它实际是完全中止了两个循环。 - -下面是 for 循环的一个例子: - -```java -// control/LabeledFor.java -// For loops with "labeled break"/"labeled continue." - -public class LabeledFor { - public static void main(String[] args) { - int i = 0; - outer: // Can't have statements here - for(; true ;) { // infinite loop - inner: // Can't have statements here - for(; i < 10; i++) { - System.out.println("i = " + i); - if(i == 2) { - System.out.println("continue"); - continue; - } - if(i == 3) { - System.out.println("break"); - i++; // Otherwise i never - // gets incremented. - break; - } - if(i == 7) { - System.out.println("continue outer"); - i++; // Otherwise i never - // gets incremented. - continue outer; - } - if(i == 8) { - System.out.println("break outer"); - break outer; - } - for(int k = 0; k < 5; k++) { - if(k == 3) { - System.out.println("continue inner"); - continue inner; - } - } - } - } - // Can't break or continue to labels here - } -} -/* Output: -i = 0 -continue inner -i = 1 -continue inner -i = 2 -continue -i = 3 -break -i = 4 -continue inner -i = 5 -continue inner -i = 6 -continue inner -i = 7 -continue outer -i = 8 -break outer -*/ -``` - -注意 **break** 会中断 **for** 循环,而且在抵达 **for** 循环的末尾之前,递增表达式不会执行。由于 **break** 跳过了递增表达式,所以递增会在 **i==3** 的情况下直接执行。在 **i==7** 的情况下,**continue outer** 语句也会到达循环顶部,而且也会跳过递增,所以它也是直接递增的。 - -如果没有 **break outer** 语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于 **break** 本身只能中断最内层的循环(对于 **continue** 同样如此)。 当然,若想在中断循环的同时退出方法,简单地用一个 **return** 即可。 - -下面这个例子向大家展示了带标签的 **break** 以及 **continue** 语句在 **while** 循环中的用法: - -```java -// control/LabeledWhile.java -// "While" with "labeled break" and "labeled continue." - -public class LabeledWhile { - public static void main(String[] args) { - int i = 0; - outer: - while(true) { - System.out.println("Outer while loop"); - while(true) { - i++; - System.out.println("i = " + i); - if(i == 1) { - System.out.println("continue"); - continue; - } - if(i == 3) { - System.out.println("continue outer"); - continue outer; - } - if(i == 5) { - System.out.println("break"); - break; - } - if(i == 7) { - System.out.println("break outer"); - break outer; - } - } - } - } -} -/* Output: -Outer while loop -i = 1 -continue -i = 2 -i = 3 -continue outer -Outer while loop -i = 4 -i = 5 -break -Outer while loop -i = 6 -i = 7 -break outer -*/ -``` - -同样的规则亦适用于 **while**: - -(1) 简单的一个 **continue** 会退回最内层循环的开头(顶部),并继续执行。 - -(2) 带有标签的 **continue** 会到达标签的位置,并重新进入紧接在那个标签后面的循环。 - -(3) **break** 会中断当前循环,并移离当前标签的末尾。 - -(4) 带标签的 **break** 会中断当前循环,并移离由那个标签指示的循环的末尾。 - -大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 **break** 或 **continue**。 - -**break** 和 **continue** 标签已经成为相对少用的推测特征(在前面的语言中很少或没有先例),所以你很少在代码里看到它们。 - -在 **Dijkstra** 的 **"goto 有害"** 论中,他最反对的就是标签,而非 **goto**。随着标签在一个程序里数量的增多,他发现产生错误的机会也越来越多。标签和 **goto** 使我们难于对程序作静态分析。但是,Java 标签不会造成这方面的问题,因为它们的应用场合已经收到了限制,没有特别的方式用于概念程序的控制。由此也引出了一个有趣的问题:通过限制语句的能力,反而能使一项语言特性更加有用。 - -## switch - -**switch** 有时也被划归为一种选择语句。根据整数表达式的值,**switch** 语句可以从一系列代码中选出一段去执行。它的格式如下: - -```java -switch(integral-selector) { - case integral-value1 : statement; break; - case integral-value2 : statement; break; - case integral-value3 : statement; break; - case integral-value4 : statement; break; - case integral-value5 : statement; break; - // ... - default: statement; -} -``` - -其中,integral-selector (整数选择因子)是一个能够产生整数值的表达式,**switch** 能够将这个表达式的结果与每个 integral-value (整数值)相比较。若发现相符的,就执行对应的语句(简单或复合语句,其中并不需要括号)。若没有发现相符的,就执行 default 语句。 - -在上面的定义中,大家会注意到每个 **case** 均以一个 **break** 结尾。这样可使执行流程跳转至 switch 主体的末尾。这是构建 **switch** 语句的一种传统方式,但 **break** 是可选的。若省略 break, 会继续执行后面的 **case** 语句的代码,直到遇到一个 **break** 为止。尽管通常不想出现这种情况,但对有经验的程序员来说,也许能够善加利用。注意最后的 **default** 语句没有 **break**,因为执行流程已到了break的跳转目的地。当然,如果考虑到编程风格方面的原因,完全可以在**default** 语句的末尾放置一个 **break**,尽管它并没有任何实际的用处。 - -switch 语句是实现多路选择的一种干净利落的一种方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是 int 或 char 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 switch 语句里是不会工作的。对于非整数类型,则必须使用一系列 if 语句。 - -下面这个例子可随机生成字母,并判断它们是元音还是辅音字母: - -```java -// control/VowelsAndConsonants.java - -// Demonstrates the switch statement -import java.util.*; - -public class VowelsAndConsonants { - public static void main(String[] args) { - Random rand = new Random(47); - for(int i = 0; i < 100; i++) { - int c = rand.nextInt(26) + 'a'; - System.out.print((char)c + ", " + c + ": "); - switch(c) { - case 'a': - case 'e': - case 'i': - case 'o': - case 'u': System.out.println("vowel"); - break; - case 'y': - case 'w': System.out.println("Sometimes vowel"); - break; - default: System.out.println("consonant"); - } - } - } -} -/* Output: (First 13 Lines) -y, 121: Sometimes vowel -n, 110: consonant -z, 122: consonant -b, 98: consonant -r, 114: consonant -n, 110: consonant -y, 121: Sometimes vowel -g, 103: consonant -c, 99: consonant -f, 102: consonant -o, 111: vowel -w, 119: Sometimes vowel -z, 122: consonant -... -*/ -``` - -由于 **Random.nextInt(26)** 会产生 0 到 26 之间的一个值,所以在其上加上一个偏移量 "a",即可产生小写字母。在 **case** 语句中,使用单引号引起的字符也会产生用于比较的整数值。 - -请注意 **case** 语句能够堆叠在一起,为一段代码形成多重匹配,即只要符合多种条件中的一种,就执行那段特别的代码。这时也应该注意将 **break** 语句置于特定 **case** 的末尾,否则控制流程会简单地下移,处理后面的 **case**。 - -在下面的语句中: - -```java -int c = rand.nextInt(26) + 'a'; -``` - -此处 **Random.nextInt()** 将产生 0~25 之间的一个随机 **int** 值,它将被加到 **a** 上。这表示 **a** 将自动被转换为 **int** 以执行假发。为了把 **c** 当作字符打印,必须将其转型为 **char**;否则,将产生整数输出。 - - - -## switch字符串 - -Java 7 增加了在字符串上 **switch** 的用法。 此示例展示了您从一组 String 可能性中选择的旧方法,以及使用 switch 的新方法: - -```java -// control/StringSwitch.java - -public class StringSwitch { - public static void main(String[] args) { - String color = "red"; - // Old way: using if-then - if("red".equals(color)) { - System.out.println("RED"); - } else if("green".equals(color)) { - System.out.println("GREEN"); - } else if("blue".equals(color)) { - System.out.println("BLUE"); - } else if("yellow".equals(color)) { - System.out.println("YELLOW"); - } else { - System.out.println("Unknown"); - } - // New way: Strings in switch - switch(color) { - case "red": - System.out.println("RED"); - break; - case "green": - System.out.println("GREEN"); - break; - case "blue": - System.out.println("BLUE"); - break; - case "yellow": - System.out.println("YELLOW"); - break; - default: - System.out.println("Unknown"); - break; - } - } -} -/* Output: -RED -RED -*/ -``` - -一旦理解了 switch,这种语法就是一个逻辑扩展。 结果更清晰,更易于理解和维护。 - -作为 **switch** 字符串的第二个例子,我们重新访问 Math.random()。 它是否产生从 0 到 1 的值,包括还是不包括值 "1"? 在数学术语中,是(0,1),还是 [0,1],还是(0,1)还是 [0,1]? (方括号表示"包括",而括号表示"不包括"。) - -下面一个可能提供答案的测试程序。 所有命令行参数都作为 **String** 对象传递,因此我们可以 **switch** 参数来决定要做什么。 有一个问题:用户可能不提供任何参数,因此索引到 args 数组会导致程序失败。 要解决这个问题,我们检查数组的长度,如果它为零,我们使用一个空字符串,否则我们选择 **args** 数组中的第一个元素: - -```java -// control/RandomBounds.java - -// Does Math.random() produce 0.0 and 1.0? -// {java RandomBounds lower} -import onjava.*; - -public class RandomBounds { - public static void main(String[] args) { - new TimedAbort(3); - switch(args.length == 0 ? "" : args[0]) { - case "lower": - while(Math.random() != 0.0) - ; // Keep trying - System.out.println("Produced 0.0!"); - break; - case "upper": - while(Math.random() != 1.0) - ; // Keep trying - System.out.println("Produced 1.0!"); - break; - default: - System.out.println("Usage:"); - System.out.println("\tRandomBounds lower"); - System.out.println("\tRandomBounds upper"); - System.exit(1); - } - } -} -``` - -要运行该程序,请键入以下任一命令: - -```java -java RandomBounds lower -or -java RandomBounds upper -``` - -使用 onjava 包中的 TimedAbort 类,程序在三秒后中止,因此看起来 Math.random() 从不生成 0.0 或 1.0。 但这就是这样一个实验可以欺骗的地方。 如果考虑0到1之间所有不同 **double** 类型的分数(double fractions)的数量,实验中达到任何一个值的可能性可能超过一台计算机甚至一个实验者的寿命。 结果是 0.0 包含在 **Math.random()** 的输出中,而 1.0 则不包括在内。 在数学术语中,它是 [0,1)。 您必须小心分析您的实验并了解它们的局限性。结果是 0.0 包含在 **Math.random()** 的输出中,而 1.0 则不包括在内。 在数学术语中,它是 [0,1)。 您必须小心分析您的实验并了解它们的局限性。 - - - -## 本章小结 - -本章总结了我们对大多数编程语言中出现的基本特征的探索:计算,运算符优先级,类型转换,选择和迭代。 现在,您已准备好开始采取措施,使您更接近面向对象和函数式编程的世界。 下一章将介绍初始化和清理对象的重要问题,接下来的章节将介绍隐藏实现细节(implementation hiding)的这一核心概念。 - - - -1. 在早期的语言中,大量的决策都是基于使编译器编写者的生活更轻松。 你会发现,在现代语言中,大多数设计决策都会让语言用户的生活更轻松,尽管有时会有妥协 - 这通常会让语言设计师感到后悔。 - -2. 请注意,这似乎是一个难以支持的主张,并且很可能是称为相关因果关系谬误的认知偏差的一个例子。 - - - - \ No newline at end of file diff --git a/book/06-Housekeeping.md b/book/06-Housekeeping.md deleted file mode 100644 index 7178d7c7..00000000 --- a/book/06-Housekeeping.md +++ /dev/null @@ -1,48 +0,0 @@ -[TOC] - - -# 第六章 初始化和清理 - - - -## 利用构造器保证初始化 - - - -## 方法重载 - - - -## 无参构造器 - - - -## this关键字 - - - -## 垃圾回收器 - - - -## 成员初始化 - - - -## 构造器初始化 - - - -## 数组初始化 - - - -## 枚举类型 - - - -## 本章小结 - - - - \ No newline at end of file diff --git a/book/07-Implementation-Hiding.md b/book/07-Implementation-Hiding.md deleted file mode 100644 index 2d13a1b2..00000000 --- a/book/07-Implementation-Hiding.md +++ /dev/null @@ -1,31 +0,0 @@ -[TOC] - - -# 第七章 封装 - - - -## 包的概念 - - - -## 访问权限修饰符 - - - -## 接口和实现 - - - -## 类访问权限 - - - -## 本章小结 - - - - - - - diff --git a/book/08-Reuse.md b/book/08-Reuse.md deleted file mode 100644 index d8b7a696..00000000 --- a/book/08-Reuse.md +++ /dev/null @@ -1,49 +0,0 @@ -[TOC] - - -# 第八章 复用 - - - -## 组合语法 - - - -## 继承语法 - - - -## 委托 - - - -## 结合组合与继承 - - - -## 组合与继承的选择 - - - -## protected - - - -## 向上转型 - - - -## final关键字 - - - -## 类初始化和加载 - - - -## 本章小结 - - - - - diff --git a/book/09-Polymorphism.md b/book/09-Polymorphism.md deleted file mode 100644 index 8692dc8c..00000000 --- a/book/09-Polymorphism.md +++ /dev/null @@ -1,32 +0,0 @@ -[TOC] - - -# 第九章 多态 - - - -## 向上转型回溯 - - - -## 深入理解 - - - -## 构造器和多态 - - - -## 返回类型协变 - - - -## 使用继承设计 - - - -## 本章小结 - - - - \ No newline at end of file diff --git a/book/10-Interfaces.md b/book/10-Interfaces.md deleted file mode 100644 index 60f6bfc8..00000000 --- a/book/10-Interfaces.md +++ /dev/null @@ -1,52 +0,0 @@ -[TOC] - - -# 第十章 接口 - - - -## 抽象类和方法 - - - -## 接口创建 - - - -## 抽象类和接口 - - - -## 完全解耦 - - - -## 多接口结合 - - - -## 使用继承扩展接口 - - - -## 接口适配 - - - -## 接口字段 - - - -## 接口嵌套 - - - -## 接口和工厂方法模式 - - - -## 本章小结 - - - - \ No newline at end of file diff --git a/book/11-Inner-Classes.md b/book/11-Inner-Classes.md deleted file mode 100644 index b2628a95..00000000 --- a/book/11-Inner-Classes.md +++ /dev/null @@ -1,60 +0,0 @@ -[TOC] - - -# 第十一章 内部类 - - - -## 创建内部类 - - - -## 链接外部类 - - - -## 内部类this和new的使用 - - - -## 内部类向上转型 - - - -## 内部类方法和作用域 - - - -## 匿名内部类 - - - -## 嵌套类 - - - -## 内部类应用场景 - - - -## 继承内部类 - - - -## 重写内部类 - - - -## 内部类局部变量 - - - -## 内部类标识符 - - - -## 本章小结 - - - - \ No newline at end of file diff --git a/book/12-Collections.md b/book/12-Collections.md deleted file mode 100644 index bf508501..00000000 --- a/book/12-Collections.md +++ /dev/null @@ -1,64 +0,0 @@ -[TOC] - - -# 第十二章 集合 - - - -## 泛型和类型安全的集合 - - - -## 基本概念 - - - -## 添加元素组 - - - -## 集合的打印 - - - -## 列表List - - - -## 迭代器Iterators - - - -## 链表LinkedList - - - -## 堆栈Stack - - - -## 集合Set - - - -## 映射Map - - - -## 队列Queue - - - -## 集合与迭代器 - - - -## for-in和迭代器 - - - -## 本章小结 - - - - \ No newline at end of file diff --git a/book/13-Functional-Programming.md b/book/13-Functional-Programming.md deleted file mode 100644 index 2eb5ea09..00000000 --- a/book/13-Functional-Programming.md +++ /dev/null @@ -1,1452 +0,0 @@ -[TOC] - - -# 第十三章 函数式编程 - - - -函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许您以函数式编程。在计算机时代的早期,内存是稀缺和珍贵的。几乎每个人都用汇编语言编程。 人们对编译器有所了解,但仅仅想到编译生成的代码肯定会比手工编码多了很多字节。 - -通常,只是为了使程序适合有限的内存,程序员通过修改内存中的代码来保存代码空间,以便在程序执行时执行不同的操作。这种技术被称为自修改代码 (self-modifying code,),只要程序足够小,少数人可以维护所有棘手和神秘的汇编代码,你就可以让它运行起来。 - -内存变得更便宜,处理器变得更快。 C语言出现并被大多数汇编语言程序员认为是"高级"。其他人发现C可以使他们显着提高生产力。 使用C,创建自修改代码仍然不是那么难。 - -随着硬件越来越便宜,程序的规模和复杂性都在增长。 只是让程序工作变得困难。 我们想方设法使代码更加一致和易懂。 自我修改代码,以其最纯粹的形式,结果是一个非常糟糕的主意,因为它很难确定它在做什么。 它也很难测试,因为你是测试输出,转换中的一些代码,修改的过程吗?诸如此类。 - -然而,使用代码以某种方式操纵其他代码的想法仍然很有趣,只要有一些方法可以使它更安全。从代码创建,维护和可靠性的角度来看,这个想法非常引人注目。 如果不是从头开始编写大量代码,而是从可以理解,经过充分测试和可靠的现有小块开始。 然后将它们组合在一起以创建新代码。 这不会让我们更有效率,同时创造更强大的代码吗? - -这就是函数式编程(FP)的意义所在。 通过合并现有代码来生成新功能而不是从头开始编写所有内容,您可以更快地获得更可靠的代码。 至少在某些情况下,这个理论似乎很有用。 在路上,函数式语言产生了很好的语法,一些非函数式语言已经习惯了。 - -你也可以这样想: - -OO (object oriented) 是抽象数据,FP (functional programming) 是抽象行为。 - -纯粹的函数式语言在安全性方面更进一步。 它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。 将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。 当强制执行此操作时,您知道任何错误都不是由所谓的副作用引起的,因为该函数仅创建并返回结果,而不是其他任何错误。 - -更好的是,"不可变对象和无副作用"范例解决了并发编程中最基本和最棘手的问题之一(当程序的某些部分同时在多个处理器上运行时)。 这是可变共享状态的问题,这意味着代码的不同部分(在不同的处理器上运行)可以尝试同时修改同一块内存(谁赢了?没人知道)。 如果函数永远不会修改现有值但只生成新值,则不会对内存产生争用,这是纯函数式语言的定义。 因此,经常提出纯函数式语言作为并行编程的解决方案(还有其他可行的解决方案)。 - -那么,请注意,函数式语言背后有很多动机,这意味着描述它们可能会有些混乱。 它通常取决于观点。 原因是"它是并行编程","代码可靠性"和"代码创建和库重用" 。[^1] 还要记住FP (functional programming)的参数 ( 特别是程序员将更快地创建更强大的代码 ) 至少仍然存在部分假设。 我们已经看到了一些好的结果,[^2]但我们没说纯函数式语言是解决编程问题的最佳方法。 - -FP的想法值得融入非FP语言。 例如,这种情况发生在Python语言中。 Java 8在FP中添加了自己的功能,我们将在此章探讨。 - - - -## 旧vs新 - - -通常,方法会根据我们传递的数据产生不同的结果。 如果您希望某个方法在从一个调用到下一个调用时表现不同,该怎么办? 如果我们将代码传递给方法,我们可以控制它的行为。 以前,我们通过在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作。 以下示例显示了这一点,然后添加了 Java 8方法:方法引用和 lambda 表达式。 - -```java -// functional/Strategize.java - -interface Strategy { - String approach(String msg); -} - -class Soft implements Strategy { - public String approach(String msg) { - return msg.toLowerCase() + "?"; - } -} - -class Unrelated { - static String twice(String msg) { - return msg + " " + msg; - } -} - -public class Strategize { - Strategy strategy; - String msg; - Strategize(String msg) { - strategy = new Soft(); // [1] - this.msg = msg; - } - void communicate() { - System.out.println(strategy.approach(msg)); - } - void changeStrategy(Strategy strategy) { - this.strategy = strategy; - } - public static void main(String[] args) { - Strategy[] strategies = { - new Strategy() { // [2] - public String approach(String msg) { - return msg.toUpperCase() + "!"; - } - }, - msg -> msg.substring(0, 5), // [3] - Unrelated::twice // [4] - }; - Strategize s = new Strategize("Hello there"); - s.communicate(); - for(Strategy newStrategy : strategies) { - s.changeStrategy(newStrategy); // [5] - s.communicate(); // [6] - } - } -} -/* Output: -hello there? -HELLO THERE!' -Hello -Hello there Hello there -*/ -``` - -**Strategy** 提供了在其单一 **approach()** 方法中承载功能的接口。 通过创建不同的 **Strategy** 对象,您可以创建不同的行为。 - -传统上,我们通过创建一个实现 **Strategy** 接口的类来实现此行为,比如在 **Soft** 。 - -**[1] **在**Strategize** 中,您可以看到 **Soft** 是默认策略,因为它是在构造函数中分配的。 - -**[2]** 一种略显冗长且更自发的方法是创建一个匿名的内部类。仍然有相当数量的重复代码,你总是要看它,直到你说 " 哦,我明白,他们正在使用一个匿名的内部类。" - -**[3] **这是 **Java 8 lambda** 表达式,由箭头 **->** 分隔开参数和函数体,箭头左边是参数,箭头右侧是从lambda返回的表达式,即函数体。这实现了与类定义和匿名内部类相同的效果,但代码少得多。 - -**[4]** 这是Java 8 的*方法引用*,由 **::** 区分。在 **::** 的左边是类或对象的名称,在 **::** 的右边是方法的名称,但没有参数列表。 - -**[5]** 在使用默认的 **Soft** **strategy** 之后,我们逐步遍历数组中的所有策略,并使用 **changeStrategy()**将每个策略放入 **s** 中。 - -[6]现在,每次调用**communic()**都会产生不同的行为,具体取决于此刻正在使用的策略 "代码对象"。我们传递行为,而不是仅传递数据。[^3] - -在Java 8之前,我们已经能够通过[1]和[2]传递功能。但是,写入和读取的语法非常笨拙,我们只在强制时才这样做。 方法引用和lambda表达式可以在您需要时传递功能,而不是仅在必要时传递功能。 - - - -## Lambda表达式 - - -Lambda表达式是使用最小可能语法编写的函数定义: - -1. Lambda 表达式产生函数,而不是类。 在 Java 虚拟机(JVM)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 - 但作为程序员,你可以高兴地假装它们"只是函数"。 - -2. lambda语法尽可能多,这正是为了使 lambda 易于编写和使用。 - -您在 **Strategize.java** 中看到了一个 lambda 表达式,但还有其他语法变体: - -```java -// functional/LambdaExpressions.java - -interface Description { - String brief(); -} - -interface Body { - String detailed(String head); -} - -interface Multi { - String twoArg(String head, Double d); -} - -public class LambdaExpressions { - - static Body bod = h -> h + " No Parens!"; // [1] - - static Body bod2 = (h) -> h + " More details"; // [2] - - static Description desc = () -> "Short info"; // [3] - - static Multi mult = (h, n) -> h + n; // [4] - - static Description moreLines = () -> { // [5] - System.out.println("moreLines()"); - return "from moreLines()"; - }; - - public static void main(String[] args) { - System.out.println(bod.detailed("Oh!")); - System.out.println(bod2.detailed("Hi!")); - System.out.println(desc.brief()); - System.out.println(mult.twoArg("Pi! ", 3.14159)); - System.out.println(moreLines.brief()); - } -} -/* Output: -Oh! No Parens! -Hi! More details -Short info -Pi! 3.14159 -moreLines() -from moreLines() -*/ -``` - -我们从三个接口开始,每个接口都有一个单独的方法(您很快就会理解它的重要性)。 但是,每个方法都有不同数量的参数,以便演示 lambda 表达式语法。 - -任何lambda表达式的基本语法是: - -1.参数。 - -2.接着是 **->**,您可以选择将其视为 "生成"。 - -3.**->** 之后的所有东西都是方法体。 - -**[1] **当只用一个参数,可以不需要括号。 然而,这是一个特例。 - -**[2]** 正常情况是在参数周围使用括号。 为了保持一致性,您还可以在单个参数周围使用括号,虽然这种情况并不常见。 - -**[3]** 如果没有参数,则必须使用括号表示空参数列表。 - -**[4]** 对于多个参数,将它们放在带括号的参数列表中。 - -到目前为止,所有 lambda 表达式方法体都是单行。 该表达式的结果自动成为 lambda 表达式的返回值,在此处使用 **return** 关键字是非法的。 这是 lambda 表达式缩写用于描述功能的语法的另一种方式。 - -**[5]** 如果在 lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 **return**。 - -Lambda 表达式通常比**匿名内部类**产生更易读的代码,因此我们将在本书中尽可能使用它们。 - -### 递归 - -递归函数是一个自我调用的函数。 可以编写递归的 lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。 我们将为每个案例创建一个示例。 - -这两个示例都需要一个接受 **int** 并生成 **int** 的接口: - -```java -// functional/IntCall.java - -interface IntCall { - int call(int arg); -} -``` - -整数 n 的阶乘将所有小于或等于 n 的正整数相乘。 阶乘函数是一个常见的递归示例: - -```java -// functional/RecursiveFactorial.java - -public class RecursiveFactorial { - static IntCall fact; - public static void main(String[] args) { - fact = n -> n == 0 ? 1 : n * fact.call(n - 1); - for(int i = 0; i <= 10; i++) - System.out.println(fact.call(i)); - } -} -/* Output: -1 -1 -2 -6 -24 -120 -720 -5040 -40320 -362880 -3628800 -*/ -``` - -这里,**fact** 是一个静态变量。 注意使用三元 **if-else**。 递归函数将一直调用自己,直到 **i == 0**.所有递归函数都有某种 "停止条件",否则它们将无限递归并产生异常。 - -我们可以将 **Fibonacci 序列** 实现为递归 lambda 表达式,这次使用实例变量: - -```java -// functional/RecursiveFibonacci.java - -public class RecursiveFibonacci { - IntCall fib; - RecursiveFibonacci() { - fib = n -> n == 0 ? 0 : - n == 1 ? 1 : - fib.call(n - 1) + fib.call(n - 2); - } - int fibonacci(int n) { return fib.call(n); } - public static void main(String[] args) { - RecursiveFibonacci rf = new RecursiveFibonacci(); - for(int i = 0; i <= 10; i++) - System.out.println(rf.fibonacci(i)); - } -} -/* Output: -0 -1 -1 -2 -3 -5 -8 -13 -21 -34 -55 -*/ -``` - -将Fibonacci 序列对中的最后两个元素求和来产生下一个元素。 - - -## 方法引用 - - -Java 8 方法引用指的是没有以前版本的 Java 所需的额外包袱的方法。 方法引用是类名或对象名,后面跟 :: [^4],然后是方法的名称。 - -```java -// functional/MethodReferences.java - -import java.util.*; - -interface Callable { // [1] - void call(String s); -} - -class Describe { - void show(String msg) { // [2] - System.out.println(msg); - } -} - -public class MethodReferences { - static void hello(String name) { // [3] - System.out.println("Hello, " + name); - } - static class Description { - String about; - Description(String desc) { about = desc; } - void help(String msg) { // [4] - System.out.println(about + " " + msg); - } - } - static class Helper { - static void assist(String msg) { // [5] - System.out.println(msg); - } - } - public static void main(String[] args) { - Describe d = new Describe(); - Callable c = d::show; // [6] - c.call("call()"); // [7] - - c = MethodReferences::hello; // [8] - c.call("Bob"); - - c = new Description("valuable")::help; // [9] - c.call("information"); - - c = Helper::assist; // [10] - c.call("Help!"); - } -} -/* Output: -call() -Hello, Bob -valuable information -Help! -*/ -``` - -**[1]** 我们从单一方法接口开始(同样,您很快就会了解到这一点的重要性)。 - -**[2]** **show()**的签名(参数类型和返回类型)符合 **Callable** 的 **call()**的签名。 - -**[3]** **hello()**也符合 **call()**的签名。 - -[4] ...就像是**help()**,一个静态内部类中的非静态方法。(原文:... as is help(), a non-static method within a static inner class.) - -**[5]** **assist()**是静态内部类中的静态方法。 - -**[6]** 我们将 **Describe** 对象的方法引用分配给 **Callable** , 它没有**show()**方法,而是 **call()**方法。 但是,Java似乎接受用这个看似奇怪的赋值,因为方法引用符合 **Callable** 的 **call()**方法的签名。 - -**[7]** 我们现在可以通过调用 **call()**来调用 **show()**,因为 Java 将 **call()**映射到 **show()**。 - -**[8]** 这是一个静态方法引用。 - -**[9] **这是 **[6]** 的另一个版本:附加到存活对象的方法的方法参考,有时称为*绑定方法引用*。 - -**[10]** 最后,获取静态内部类的静态方法的方法引用,用起来就像 **[8] **中的外部类。 - -这不是一个详尽的例子; 我们很快就会看到方法参考的所有变化。 - -### Runnable - -**Runnable** 接口自 1.0 版以来一直在 Java 中,因此不需要导入。 它也符合特殊的单方法接口格式:它的方法run()不带参数,也没有返回值。 因此,我们可以使用 lambda 表达式和方法引用作为 **Runnable**: - -```java -// functional/RunnableMethodReference.java - -// Method references with interface Runnable - -class Go { - static void go() { - System.out.println("Go::go()"); - } -} - -public class RunnableMethodReference { - public static void main(String[] args) { - - new Thread(new Runnable() { - public void run() { - System.out.println("Anonymous"); - } - }).start(); - - new Thread( - () -> System.out.println("lambda") - ).start(); - - new Thread(Go::go).start(); - } -} -/* Output: -Anonymous -lambda -Go::go() -*/ -``` - -**Thread** 对象将 **Runnable** 作为其构造函数参数,并具有会调用 **run()**的方法 **start()**。 请注意,只有**匿名内部类**才需要具有名为**run()**的方法。 - - - -### 未绑定的方法引用 - -未绑定的方法引用是指没有关联对象的普通(非静态)方法。 要使用未绑定的引用,您必须提供以下对象: - -```java -// functional/UnboundMethodReference.java - -// Method reference without an object - -class X { - String f() { return "X::f()"; } -} - -interface MakeString { - String make(); -} - -interface TransformX { - String transform(X x); -} - -public class UnboundMethodReference { - public static void main(String[] args) { - // MakeString ms = X::f; // [1] - TransformX sp = X::f; - X x = new X(); - System.out.println(sp.transform(x)); // [2] - System.out.println(x.f()); // Same effect - } -} -/* Output: -X::f() -X::f() -*/ -``` - -到目前为止,我们已经看到了对与其关联接口具有相同签名的方法的引用。 在 **[1]**,我们尝试对X中的 **f()**做同样的事情,试图分配给 **MakeString**。 这会产生编译器关于"无效方法引用"的错误 ("invalid method reference),即使 **make()**与**f()**具有相同的签名。 问题是实际上还有另一个(隐藏的)参数:我们的老朋友 **this**。 你不能在没有 **X** 对象的情况下调用 **f()**来调用它。 因此,**X :: f** 表示未绑定的方法引用,因为它尚未"绑定"到对象。 - -要解决这个问题,我们需要一个 **X** 对象,所以我们的接口实际上需要一个额外的参数,就像你在 **TransformX** 中看到的那样。 如果将 **X :: f** 分配给 **TransformX**,Java 很高兴。 我们现在必须进行第二次心理调整 - 使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 有一个很好的理由说服你,那就是你需要一个对象来调用方法。 - -**[2]** 的结果有点像脑筋急转弯。 我接受未绑定的引用并对其调用 **transform()**,将其传递给X,并以某种方式导致对 **x.f()**的调用。 Java知道它必须采用第一个参数,实际上是这个,并在其上调用方法。 - -```java -// functional/MultiUnbound.java - -// Unbound methods with multiple arguments - -class This { - void two(int i, double d) {} - void three(int i, double d, String s) {} - void four(int i, double d, String s, char c) {} -} - -interface TwoArgs { - void call2(This athis, int i, double d); -} - -interface ThreeArgs { - void call3(This athis, int i, double d, String s); -} - -interface FourArgs { - void call4( - This athis, int i, double d, String s, char c); -} - -public class MultiUnbound { - public static void main(String[] args) { - TwoArgs twoargs = This::two; - ThreeArgs threeargs = This::three; - FourArgs fourargs = This::four; - This athis = new This(); - twoargs.call2(athis, 11, 3.14); - threeargs.call3(athis, 11, 3.14, "Three"); - fourargs.call4(athis, 11, 3.14, "Four", 'Z'); - } -} -``` - -为了说明这一点,我将类命名为 **This** ,函数方法的第一个参数则是 **athis**,但是您应该选择其他名称以防止生产代码混淆。 - -### 构造函数引用 - -您还可以捕获构造函数的引用,然后通过引用调用该构造函数。 - -```java -// functional/CtorReference.java - -class Dog { - String name; - int age = -1; // For "unknown" - Dog() { name = "stray"; } - Dog(String nm) { name = nm; } - Dog(String nm, int yrs) { name = nm; age = yrs; } -} - -interface MakeNoArgs { - Dog make(); -} - -interface Make1Arg { - Dog make(String nm); -} - -interface Make2Args { - Dog make(String nm, int age); -} - -public class CtorReference { - public static void main(String[] args) { - MakeNoArgs mna = Dog::new; // [1] - Make1Arg m1a = Dog::new; // [2] - Make2Args m2a = Dog::new; // [3] - - Dog dn = mna.make(); - Dog d1 = m1a.make("Comet"); - Dog d2 = m2a.make("Ralph", 4); - } -} -``` - -**Dog** 有三个构造函数,函数接口内的 **make()**方法反映了构造函数参数列表( make()方法可以有不同的名称)。 - -注意我们如何对 **[1]**,**[2]** 和 **[3]** 中的每一个使用 **Dog :: new**。 所有三个构造函数只有一个名称:**:: new**。 但是构造函数引用在每种情况下都分配给不同的接口,并且编译器可以知道从哪个构造函数引用中进行检测。 - -编译器可以看到调用函数方法( 在本例中为make())意味着调用构造函数。 - - -## 函数式接口 - - -方法引用和lambda表达式都是必须赋值的,并且这些赋值需要编译器的类型信息以确保类型正确性。 Lambda表达式特别引入了新的要求。 考虑: - -```java -x -> x.toString() -``` - -我们看到返回类型必须是String,但x是什么类型? - -因为 lambda 表达式包含一种类型推断形式(编译器会对类型进行描述,而不是要求程序员显式),编译器必须能够以某种方式推导出 x 的类型。 - -这是第二个例子: - -```java -(x, y) -> x + y -``` - -现在 **x** 和 **y** 可以是支持 **+** 运算符的任何类型,包括两个不同的数字类型或一个 **String** 以及一些将自动转换为**String** 的类型(这包括大多数类型)。 但是,当分配此 lambda 表达式时,编译器必须确定 **x** 和 **y** 的确切类型以生成正确的代码。 - -同样的问题适用于方法引用。 假设你要传递 System.out :: println 到你正在编写的方法 ,你为方法的参数给出了什么类型? - -为了解决这个问题,Java 8 引入了 **java.util.function**,它包含一组接口,这些接口是 lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。 - -在编写接口时,可以使用 **@FunctionalInterface** 注释强制执行此"函数方法"模式: - -```java -// functional/FunctionalAnnotation.java - -@FunctionalInterface -interface Functional { - String goodbye(String arg); -} - -interface FunctionalNoAnn { - String goodbye(String arg); -} - -/* -@FunctionalInterface -interface NotFunctional { - String goodbye(String arg); - String hello(String arg); -} -Produces error message: -NotFunctional is not a functional interface -multiple non-overriding abstract methods -found in interface NotFunctional -*/ - -public class FunctionalAnnotation { - public String goodbye(String arg) { - return "Goodbye, " + arg; - } - public static void main(String[] args) { - FunctionalAnnotation fa = - new FunctionalAnnotation(); - Functional f = fa::goodbye; - FunctionalNoAnn fna = fa::goodbye; - // Functional fac = fa; // Incompatible - Functional fl = a -> "Goodbye, " + a; - FunctionalNoAnn fnal = a -> "Goodbye, " + a; - } -} -``` - -**@FunctionalInterface **注释是可选的; Java 将 **Functional** 和 **FunctionalNoAnn** 视为 **main()**中的函数接口。 **@FunctionalInterface** 的值在 **NotFunctional** 的定义中可见。接口中的如果有多个方法则会产生编译时错误消息。 - -仔细观察 **f** 和 **fna** 定义中会发生什么。 **Functional** 和 **FunctionalNoAnn** 定义接口。 然而,分配的只是方法 **goodbye**()。首先,这只是一种方法而不是一种类。 其次,它甚至不是一个实现了该接口的类里的方法。这是添加到 Java 8 中的一些魔力:如果将方法引用或 lambda 表达式分配给函数接口(以及类型适合),Java将使您的分配适应目标接口。 在背后,编译器将方法引用或 lambda 表达式包装在实现目标接口的类的实例中。 - -尽管 **FunctionalAnnotation** 确实适合 **Functional** 模型,但如果我们尝试将 **FunctionalAnnotation** 直接分配给 **Functional**,就像 **fac** 的定义一样,Java 将不会让我们成功,因为它没有明确地实现 **Functional** 接口。 但令人惊讶的是 ,Java8 允许我们为接口分配函数,从而产生更好,更简单的语法。 - -**java.util.function** 的目标是创建一组完整的目标接口,这样您通常不需要定义自己的接口。 主要是因为原始类型,这会产生一小部分接口。 如果您了解命名模式,通常可以通过查看名称来检测特定接口的作用。 - - 以下是基本命名准则: - -1. 如果它只处理对象而不是原语,那么它只是一个简单的名称,如 **Function**,**Consumer**,**Predicate** 等。参数类型通过泛型添加。 - -2. 如果它采用原始参数,则由名称的第一部分表示,如 **LongConsumer**,**DoubleFunction**,**IntPredicate** 等。例外情况是原始的供应商类型。 - -3. 如果它返回原始结果,则用 **To** 表示,如 **ToLongFunction ** 和 **IntToLongFunction**。 - -4. 如果它返回与其参数相同的类型,则它是一个运算符,其中一个参数使用 **UnaryOperator**,两个参数使用 **BinaryOperator**。 - -5. 如果它需要两个参数并返回一个布尔值,那么它就是一个谓词( Predicate )。 - -6. 如果它需要两个不同类型的参数,则名称中有一个 **Bi**。 - -该表描述了java.util.function中的目标类型 - -........下面的表格内容,直接复制PDF文档下来很难看懂。( 待整理 ) - -(with noted exceptions): - -**Name Characteristic Functional Usage Method Runnable** No arguments; - - (java.lang) **Runnable** Returns nothing **run() Supplier Supplier BooleanSupplier** No arguments; - -**get() IntSupplier** Returns any type getAstype() LongSupplier DoubleSupplier Callable No arguments; (java.util.concurrent) Callable Returns any type call() Consumer One argument; - -Consumer IntConsumer Returns nothing accept() LongConsumer DoubleConsumer - -Two-argument BiConsumer BiConsumer Consumer accept() Two-argument Consumer; ObjIntConsumer First arg is a ObjtypeConsumer ObjLongConsumer reference; accept() Second arg is a ObjDoubleConsumer primitive Function IntFunction LongFunction DoubleFunction ToIntFunction Function One argument; ToLongFunction apply() Returns a different ToDoubleFunction type Totype & typeTotype: IntToLongFunction applyAstype() IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction UnaryOperator One argument; UnaryOperator IntUnaryOperator Returns the same type apply() LongUnaryOperator DoubleUnaryOperator BinaryOperator Two arguments, same type; BinaryOperator IntBinaryOperator Returns the same apply() LongBinaryOperator type DoubleBinaryOperator Two arguments, Comparator same type; (java.util) Comparator Returns int compare() Predicate BiPredicate Two arguments; Predicate IntPredicate Returns boolean test() LongPredicate DoublePredicate IntToLongFunction IntToDoubleFunction Primitive argument; typeTotypeFunction LongToIntFunction Returns a primitive applyAstype() LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction BiFunction BiConsumer Two arguments; Bioperation BiPredicate Different types (method name varies) ToIntBiFunction ToLongBiFunction ToDoubleBiFunction - - - -您可能会想到用于进一步添加更多行,但此表提供了基本概念,并且应该帮到或多或少地推断出您需要的函数接口。 - -您可以看到在创建 **java.util.function** 时做出了一些选择。 例如,为什么没有 **IntComparator**,**LongComparator** 和 **DoubleComparator** ? 有一个 **BooleanSupplier**,但没有其他接口表示 **Boolean**。 有一个通用的 **BiConsumer**,但没有用于所有 **int**,**long** 和 **double** 的 **BiConsumers** 变体( 我可以支持他们为什么放弃那个)。 这是疏忽还是有人决定(他们是如何得出这个结论的)? - -您还可以看到原始类型为 Java 添加了多少复杂性。 由于效率问题,它们被包含在该语言的第一版中 - 这很快就得到了缓解。 现在,在语言的生命周期中,我们仍然受到语言设计选择不佳的影响。 - -下面枚举基于lambda表达式的所有不同 Function 变体的示例: - -```java -// functional/FunctionVariants.java - -import java.util.function.*; - -class Foo {} - -class Bar { - Foo f; - Bar(Foo f) { this.f = f; } -} - -class IBaz { - int i; - IBaz(int i) { - this.i = i; - } -} - -class LBaz { - long l; - LBaz(long l) { - this.l = l; - } -} - -class DBaz { - double d; - DBaz(double d) { - this.d = d; - } -} - -public class FunctionVariants { - static Function f1 = f -> new Bar(f); - static IntFunction f2 = i -> new IBaz(i); - static LongFunction f3 = l -> new LBaz(l); - static DoubleFunction f4 = d -> new DBaz(d); - static ToIntFunction f5 = ib -> ib.i; - static ToLongFunction f6 = lb -> lb.l; - static ToDoubleFunction f7 = db -> db.d; - static IntToLongFunction f8 = i -> i; - static IntToDoubleFunction f9 = i -> i; - static LongToIntFunction f10 = l -> (int)l; - static LongToDoubleFunction f11 = l -> l; - static DoubleToIntFunction f12 = d -> (int)d; - static DoubleToLongFunction f13 = d -> (long)d; - - public static void main(String[] args) { - Bar b = f1.apply(new Foo()); - IBaz ib = f2.apply(11); - LBaz lb = f3.apply(11); - DBaz db = f4.apply(11); - int i = f5.applyAsInt(ib); - long l = f6.applyAsLong(lb); - double d = f7.applyAsDouble(db); - l = f8.applyAsLong(12); - d = f9.applyAsDouble(12); - i = f10.applyAsInt(12); - d = f11.applyAsDouble(12); - i = f12.applyAsInt(13.0); - l = f13.applyAsLong(13.0); - } -} -``` - -这些 **lambda** 表达式尝试生成适合对应函数签名的最简代码。 在某些情况下,强制转换是必要的,否则编译器会抱怨截断错误。 - -**main()**中的每个测试都显示了 **Function** 接口中不同类型的 **apply** 方法。 每个都产生一个对其相关的**lambda** 表达式的调用。 - -方法引用有自己的魔力: - -```java -/ functional/MethodConversion.java - -import java.util.function.*; - -class In1 {} -class In2 {} - -public class MethodConversion { - static void accept(In1 i1, In2 i2) { - System.out.println("accept()"); - } - static void someOtherName(In1 i1, In2 i2) { - System.out.println("someOtherName()"); - } - public static void main(String[] args) { - BiConsumer bic; - - bic = MethodConversion::accept; - bic.accept(new In1(), new In2()); - - bic = MethodConversion::someOtherName; - // bic.someOtherName(new In1(), new In2()); // Nope - bic.accept(new In1(), new In2()); - } -} -/* Output: -accept() -someOtherName() -*/ -``` - -查看 **BiConsumer** 的文档。 你会看到它的函数方法是 **accept()**。 实际上,如果我们将方法命名为**accept()**,它就可以作为方法引用。 但是我们可以给它一个完全不同的名称,比如**someOtherName()**,它也可以运行,只要参数类型和返回类型与 **BiConsumer** 的 **accept()**相同。 - -因此,在使用函数接口时,名称无关紧要 - 只有参数类型和返回类型相同。 Java 将您的名称映射到接口的函数方法。 要调用方法,可以调用函数方法名称(在本例中为 **accept()**),而不是你的方法名称。 - -现在我们将查看应用于方法引用的所有基于类的 Functionals(即那些不涉及原始类型的函数)。 我再次创建了适合函数签名的最简单方法: - -```java -// functional/ClassFunctionals.java - -import java.util.*; -import java.util.function.*; - -class AA {} -class BB {} -class CC {} - -public class ClassFunctionals { - static AA f1() { return new AA(); } - static int f2(AA aa1, AA aa2) { return 1; } - static void f3(AA aa) {} - static void f4(AA aa, BB bb) {} - static CC f5(AA aa) { return new CC(); } - static CC f6(AA aa, BB bb) { return new CC(); } - static boolean f7(AA aa) { return true; } - static boolean f8(AA aa, BB bb) { return true; } - static AA f9(AA aa) { return new AA(); } - static AA f10(AA aa1, AA aa2) { return new AA(); } - public static void main(String[] args) { - Supplier s = ClassFunctionals::f1; - s.get(); - Comparator c = ClassFunctionals::f2; - c.compare(new AA(), new AA()); - Consumer cons = ClassFunctionals::f3; - cons.accept(new AA()); - BiConsumer bicons = ClassFunctionals::f4; - bicons.accept(new AA(), new BB()); - Function f = ClassFunctionals::f5; - CC cc = f.apply(new AA()); - BiFunction bif = ClassFunctionals::f6; - cc = bif.apply(new AA(), new BB()); - Predicate p = ClassFunctionals::f7; - boolean result = p.test(new AA()); - BiPredicate bip = ClassFunctionals::f8; - result = bip.test(new AA(), new BB()); - UnaryOperator uo = ClassFunctionals::f9; - AA aa = uo.apply(new AA()); - BinaryOperator bo = ClassFunctionals::f10; - aa = bo.apply(new AA(), new AA()); - } -} -``` - -请注意,每个方法名称都是任意的( **f1(),f2()**等),但正如您刚才看到的,一旦将方法引用分配给函数接口,您就可以调用与该接口关联的函数方法。 在此示例中,这些是 **get(),compare(),accept(),apply()**和 **test()**。 - - - -### 有着更多参数的函数接口 - -**java.util.functional** 中的接口是有限的。 比如有了 **BiFunction**,但它不能变化。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建: - -```java -// functional/TriFunction.java - -@FunctionalInterface -public interface TriFunction { - R apply(T t, U u, V v); -} -``` - -一个简短的测试将验证它是否有效: - -```java -// functional/TriFunctionTest.java - -public class TriFunctionTest { - static int f(int i, long l, double d) { return 99; } - public static void main(String[] args) { - TriFunction tf = - TriFunctionTest::f; - tf = (i, l, d) -> 12; - } -} -``` - -这里我们测试方法引用和 lambda 表达式。 - -### 缺少原始类型的函数 - -让我们重温一下 **BiConsumer**,看看我们如何创建缺少 **int,long** 和 **double **的各种排列: - -```java -// functional/BiConsumerPermutations.java - -import java.util.function.*; - -public class BiConsumerPermutations { - static BiConsumer bicid = (i, d) -> - System.out.format("%d, %f%n", i, d); - static BiConsumer bicdi = (d, i) -> - System.out.format("%d, %f%n", i, d); - static BiConsumer bicil = (i, l) -> - System.out.format("%d, %d%n", i, l); - public static void main(String[] args) { - bicid.accept(47, 11.34); - bicdi.accept(22.45, 92); - bicil.accept(1, 11L); - } -} -/* Output: -47, 11.340000 -92, 22.450000 -1, 11 -*/ -``` - -为了显示,我使用 **System.out.format()**,它类似于 **System.out.println()**,除了它提供了更多的显示选项。 这里,**%f** 表示我将 **n** 作为浮点值给出,**%d** 表示 **n** 是一个整数值。 我能够包含空格,并且它不会添加换行符,除非你输入%n - 它也会接受传统 **\ n** 换行符,但 **%n** 是自动跨平台的,这是使用的 **format()** 的另一个原因。 - -该示例仅使用适当的包装器类型,装箱和拆箱负责在原始类型之间来回转换。 我们也可以使用包装类型,例如 Function,而不是预定义的原始类型: - -```java -// functional/FunctionWithWrapped.java - -import java.util.function.*; - -public class FunctionWithWrapped { - public static void main(String[] args) { - Function fid = i -> (double)i; - IntToDoubleFunction fid2 = i -> i; - } -} -``` - -如果没有强制转换,则会收到错误消息:" Integer 无法转换为 Double ",而 **IntToDoubleFunction** 版本没有此类问题。 Java库代码里 **IntToDoubleFunction** 是这样子的: - -```java -@FunctionalInterface -public interface IntToDoubleFunction { - double applyAsDouble(int value); -} -``` - -因为我们可以简单地编写 **Function ** 并产生工作结果,所以很明显,函数的原始类型的唯一原因是为了防止传递参数和返回结果所涉及的自动装箱和自动装箱。 也就是说,为了性能。 - -似乎可以安全地推测,某些函数类型具有定义而其他类型没有定义是因为考虑到了使用频率。 - -当然,如果由于缺少原始类型的函数而导致性能实际上成为问题,您可以轻松编写自己的接口( 使用Java库源进行参考 ) - 尽管这似乎不太可能是您的性能瓶颈。 - - -## 高阶函数 - - -这个名字听起来有点令人生畏,但是:高阶函数只是一个消耗或产生函数的函数。 - -我们先来看看产生一个函数: - -```java -// functional/ProduceFunction.java - -import java.util.function.*; - -interface -FuncSS extends Function {} // [1] - -public class ProduceFunction { - static FuncSS produce() { - return s -> s.toLowerCase(); // [2] - } - public static void main(String[] args) { - FuncSS f = produce(); - System.out.println(f.apply("YELLING")); - } -} -/* Output: -yelling -*/ -``` - -这里,**produce()**是高阶函数。 - -**[1]** 使用继承,您可以轻松地为你的专用接口创建别名。 - -**[2]** 使用 **lambda** 表达式,在方法中创建和返回一个函数几乎毫不费力。 - -要 **consume** 函数(consume a function),其对应的 **consuming** 方法 (the consuming method)的参数列表必须正确描述函数类型: - -```java -// functional/ConsumeFunction.java - -import java.util.function.*; - -class One {} -class Two {} - -public class ConsumeFunction { - static Two consume(Function onetwo) { - return onetwo.apply(new One()); - } - public static void main(String[] args) { - Two two = consume(one -> new Two()); - } -} -``` - -当您根据 **consume** 的函数生成新函数时,事情变得特别有趣,比如: - -```java -// functional/TransformFunction.java - -import java.util.function.*; - -class I { - @Override - public String toString() { return "I"; } -} - -class O { - @Override - public String toString() { return "O"; } -} - -public class TransformFunction { - static Function transform(Function in) { - return in.andThen(o -> { - System.out.println(o); - return o; - }); - } - public static void main(String[] args) { - Function f2 = transform(i -> { - System.out.println(i); - return new O(); - }); - O o = f2.apply(new I()); - } -} -/* Output: -I -O -*/ -``` - -在这里,**transform()**生成一个与传入的函数具有相同签名的函数,但是您可以生成任何您想要的类型。这在 **Function** 接口中使用名为 **andThen()**的默认方法,该方法专门用于操作函数。 顾名思义,在调用in函数之后调用 **toThen()**(还有 **compose()**,它在 **in** 函数之前应用新函数)。 要附加一个**andThen()**函数,我们只需将该函数作为参数传递。 **transform()**产生的是一个新函数,它将 **in** 的动作与 **andThen()**参数的动作结合起来。 - - -## 闭包 - - -在上一节的 **ProduceFunction.java** 中,我们从方法返回了一个 lambda 函数。 这个例子让事情变得简单,但是我们必须在返回 lambdas 时探讨一些问题。 - -*闭包*一词概括了这些问题。 闭包非常重要,因为它们可以轻松生成函数。 - -考虑一个更复杂的 lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当您调用函数时,它对那些 "外部 "变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者在词法上限定范围( 也使用术语变量捕获 )。Java 8提供了有限但合理的闭包支持, - -我们将用一些简单的例子来研究它。 - -首先,这里有一个方法返回一个访问对象字段和方法参数的函数: - -```java -// functional/Closure1.java - -import java.util.function.*; - -public class Closure1 { - int i; - IntSupplier makeFun(int x) { - return () -> x + i++; - } -} -``` - -但是,仔细考虑一下,**i** 的这种用法并不是一个很大的挑战,因为对象很可能在您调用 **makeFun()** 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上。当然,如果你对同一个对象多次调用 **makeFun()** ,你最终会得到多个函数,它们都为 **i** 共享相同的存储空间: - -```java -// functional/SharedStorage.java - -import java.util.function.*; - -public class SharedStorage { - public static void main(String[] args) { - Closure1 c1 = new Closure1(); - IntSupplier f1 = c1.makeFun(0); - IntSupplier f2 = c1.makeFun(0); - IntSupplier f3 = c1.makeFun(0); - System.out.println(f1.getAsInt()); - System.out.println(f2.getAsInt()); - System.out.println(f3.getAsInt()); - } -} -/* Output: -0 -1 -2 -*/ -``` - -每次调用 **getAsInt()**都会增加 **i **,表明存储是共享的。 - -如果 **i** 是 **makeFun()**的本地怎么办? 在正常情况下,当 **makeFun()**完成时 **i** 就消失。 但它仍然编译: - -```java -// functional/Closure2.java - -import java.util.function.*; - -public class Closure2 { - IntSupplier makeFun(int x) { - int i = 0; - return () -> x + i; - } -} -``` - -由 **makeFun()**返回的 **IntSupplier** "关闭" **i** 和 **x**,因此当您调用返回的函数时两者仍然有效。 但请注意,我没有像 **Closure1.java** 那样增加i。 尝试递增它会产生编译时错误: - -```java -// functional/Closure3.java - -// {WillNotCompile} -import java.util.function.*; - -public class Closure3 { - IntSupplier makeFun(int x) { - int i = 0; - // Neither x++ nor i++ will work: - return () -> x++ + i++; - } -} -``` - -**x** 和 **i** 的操作都犯了同样的错误: - -显然,从 lambda 表达式引用的局部变量必须是 **final** 或者 实际的 **final** (effectively final),如果我们声明 **x**和 **i** 是 **final** ,它将起作用,因为那时我们不能增加任何一个: - -```java -// functional/Closure4.java - -import java.util.function.*; - -public class Closure4 { - IntSupplier makeFun(final int x) { - final int i = 0; - return () -> x + i; - } -} -``` - -但是为什么 **Closure2.java** 在 **x** 和 **i **不是 **final** 却可以运行? - -这就是 "实际" **final **(effectively final)的含义出现的地方。 这个术语是为 Java 8 创建的,表示你没有明确地声明变量是 **final** 的,但你仍然是这样对待它 - 你没有改变它。 如果局部变量的初始值永远不会改变,那么它实际上是最终的。 - -如果 **x** 和 **i** 在方法中的其他位置更改(但不在返回函数内部),则编译器仍将其视为错误。 每个增量产生一个单独的错误消息: - -```java -/ functional/Closure5.java - -// {WillNotCompile} -import java.util.function.*; - -public class Closure5 { - IntSupplier makeFun(int x) { - int i = 0; - i++; - x++; - return () -> x + i; - } -} -``` - -要成为 "effectively final" ,意味着您可以将 final 关键字应用于变量声明而不更改任何其余代码。 它实际上是 **final**的,你只是没有明说。 - -我们实际上可以通过在闭包中使用它们之前将 **x** 和 **i** 分配给 **final** 变量来解决 **Closure5.java** 中的问题: - -```java - -// functional/Closure6.java - -import java.util.function.*; - -public class Closure6 { - IntSupplier makeFun(int x) { - int i = 0; - i++; - x++; - final int iFinal = i; - final int xFinal = x; - return () -> xFinal + iFinal; - } -} -``` - -由于我们在分配后永远不会更改 **iFinal** 和 **xFinal** ,因此在这里使用 **final** 是多余的。 - -如果您使用引用怎么办? 我们可以从 **int** 更改为 **Integer**: - -```java -// functional/Closure7.java - -// {WillNotCompile} -import java.util.function.*; - -public class Closure7 { - IntSupplier makeFun(int x) { - Integer i = 0; - i = i + 1; - return () -> x + i; - } -} -``` - -编译器仍然足够聪明,可以看到 **i** 正在被更改。 包装器类型可能正在进行特殊处理,所以让我们尝试一下List: - -```java -// functional/Closure8.java - -import java.util.*; -import java.util.function.*; - -public class Closure8 { - Supplier> makeFun() { - final List ai = new ArrayList(); - ai.add(1); - return () -> ai; - } - public static void main(String[] args) { - Closure8 c7 = new Closure8(); - List - l1 = c7.makeFun().get(), - l2 = c7.makeFun().get(); - System.out.println(l1); - System.out.println(l2); - l1.add(42); - l2.add(96); - System.out.println(l1); - System.out.println(l2); - } -} -/* Output: -[1] -[1] -[1, 42] -[1, 96] -*/ -``` - -这次它可以运行:我们修改 **List** 的内容而没产生编译时错误。 当您查看此示例的输出时,它看起来确实非常安全,因为每次调用 **makeFun()**时,都会创建并返回一个全新的 **ArrayList** - 这意味着它不会被共享,因此每个生成的闭包都有自己独立的 **ArrayList** 他们不能互相干扰。 - -并且请注意我已经声明 **ai** 是 **final** 的,尽管在这个例子中你可以去掉 **final** 并得到相同的结果(试试吧!)。 应用于对象引用的 **final** 关键字仅表示不会重新分配引用。 它并没有说你无法修改对象本身。 - -看看 **Closure7.java** 和 **Closure8.java** 之间的区别,我们看到 **Closure7.java** 实际上有一个 **i** 的重新分配。 也许这是 "effectively final" 错误消息的触发点: - -```java -// functional/Closure9.java - -// {WillNotCompile} -import java.util.*; -import java.util.function.*; - -public class Closure9 { - Supplier> makeFun() { - List ai = new ArrayList(); - ai = new ArrayList(); // Reassignment - return () -> ai; - } -} -``` - -引用的重新分配确实会触发错误消息。 如果只修改指向的对象,Java 会接受它。 只要没有其他人获得对该对象的引用(这意味着您有多个可以修改对象的实体,此时事情会变得非常混乱),这可能是安全的。[^6] - -然而,如果我们现在回顾一下 **Closure1.java.** ,那就有一个难题:**i** 被修改却没有编译器投诉。 它既不是 **final** 的,也不是"effectively final"的。因为 **i** 是外围类的成员,所以这样做肯定是安全的( 除非你正在创建共享可变内存的多个函数)。实际上,您可以争辩说在这种情况下不会发生变量捕获(variable capture)。 可以肯定的是,**Closure3.java** 的错误消息专门针对局部变量。 因此,规则并不像说"在lambda之外定义的任何变量必须是 **final** 的或 **effectively final** 那么简单。相反,你必须考虑捕获的变量是否实际 **final**。 如果它是对象中的字段,那么它有一个独立的生存期,并且不需要任何特殊的捕获,以便稍后在调用 **lambda** 时存在。 - - - -### 作为闭包的内部类 - -我们可以复制我们的例子使用匿名内部类: - -```java -// functional/AnonymousClosure.java - -import java.util.function.*; - -public class AnonymousClosure { - IntSupplier makeFun(int x) { - int i = 0; - // Same rules apply: - // i++; // Not "effectively final" - // x++; // Ditto - return new IntSupplier() { - public int getAsInt() { return x + i; } - }; - } -} -``` - -事实证明,只要有内部类,就会有闭包(Java 8只 会使闭包变得更容易)。 在 Java 8 之前,要求是 **x** 和 **i** 被明确声明为 **final**。 使用 Java 8,内部类的规则已经放宽,包括 "effectively final"。 - - -## 函数组合 - - -函数组合基本上意味着"将函数粘贴在一起以创建新函数",它通常被认为是函数编程的一部分。您在**TransformFunction.java** 中看到了一个使用 **andThen()** 的函数组合示例。一些 **java.util** 的函数接口包含支持函数组合的方法。 - -Compositional Supporting Method Interfaces Function BiFunction Consumer BiConsumer IntConsumer andThen(argument) Performs the original LongConsumer operation followed by DoubleConsumer the argument operation. - -UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator BinaryOperator Function compose(argument) UnaryOperator Performs the argument IntUnaryOperator operation followed by the original operation. LongUnaryOperator DoubleUnaryOperator Predicate and(argument) BiPredicate Short-circuiting logical IntPredicate AND of the original predicate and the LongPredicate argument predicate. DoublePredicate Predicate or(argument) BiPredicate Short-circuiting logical OR of the original IntPredicate predicate and the LongPredicate argument predicate. DoublePredicate Predicate negate() BiPredicate A predicate that is the IntPredicate logical negation of this predicate. LongPredicate DoublePredicate - -( 待整理 ) - -此示例使用 **Function** 里的 **compose()**和 **andThen()** - -```java -// functional/FunctionComposition.java - -import java.util.function.*; - -public class FunctionComposition { - static Function - f1 = s -> { - System.out.println(s); - return s.replace('A', '_'); - }, - f2 = s -> s.substring(3), - f3 = s -> s.toLowerCase(), - f4 = f1.compose(f2).andThen(f3); - public static void main(String[] args) { - System.out.println( - f4.apply("GO AFTER ALL AMBULANCES")); - } -} -/* Output: -AFTER ALL AMBULANCES -_fter _ll _mbul_nces -*/ -``` - -这里要看的重要一点是我们正在创建一个新函数 **f4**,然后可以使用 **apply()**(几乎)像任何其他函数一样调用它。[^8] - -当 **f1** 获得String时,它已经被**f2** 剥离了前三个字符。 这是因为对 **compose(f2)**的调用意味着在 **f1** 之前调用 **f2**。 - -这是Predicate逻辑运算的演示: - -```java -// functional/PredicateComposition.java - -import java.util.function.*; -import java.util.stream.*; - -public class PredicateComposition { - static Predicate - p1 = s -> s.contains("bar"), - p2 = s -> s.length() < 5, - p3 = s -> s.contains("foo"), - p4 = p1.negate().and(p2).or(p3); - public static void main(String[] args) { - Stream.of("bar", "foobar", "foobaz", "fongopuckey") - .filter(p4) - .forEach(System.out::println); - } -} -/* Output: -foobar -foobaz -*/ -``` - -**p4** 获取所有谓词并将它们组合成一个更复杂的谓词,其中包含:"如果 **String** 不包含 'bar' 且长度小于5,或者它包含 'foo' ,则结果为 **true**。"因为它产生如此清晰的语法,我在**main()**中作了一些小伎俩,并借用了下一章的内容。 首先,我创建一个 **String** 对象的 "流"(序列),然后将每个对象提供给 **filter()**操作。 **filter()**使用我们的 **p4** 谓词来决定要保留流中的哪个对象以及要丢弃的对象。 最后,我使用 **forEach()**将 **println** 方法引用应用于每个幸存的对象。 - -你可以从输出中看到 **p4** 是如何工作的:任何带有 "foo "的东西都会存活,即使它的长度大于5。 "fongopuckey" 太长了,没有 "bar" 来保存它。 - - -## Currying和Partial-Evaluation - - -*Currying* 以 Haskell Curry 命名,Haskell Curry 是其发明者之一,可能是唯一一个以他的名字命名的重要事物的计算机领域的人物(另一个是 Haskell 编程语言)。 Currying 意味着从一个函数开始,该函数接受多个参数,并将其转换为一系列函数,每个函数只接受一个参数。 - -```java -// functional/CurryingAndPartials.java - -import java.util.function.*; - -public class CurryingAndPartials { - // Uncurried: - static String uncurried(String a, String b) { - return a + b; - } - public static void main(String[] args) { - // Curried function: - Function> sum = - a -> b -> a + b; // [1] - - System.out.println(uncurried("Hi ", "Ho")); - - Function - hi = sum.apply("Hi "); // [2] - System.out.println(hi.apply("Ho")); - - // Partial application: - Function sumHi = - sum.apply("Hup "); - System.out.println(sumHi.apply("Ho")); - System.out.println(sumHi.apply("Hey")); - } -} -/* Output: -Hi Ho -Hi Ho -Hup Ho -Hup Hey -*/ -``` - -**[1]** 这是一条巧妙的线:一连串的箭头。注意,在函数接口声明中,函数的第二个参数是另一个函数。 - -**[2]** currying 的目标是能够通过提供一个参数来创建一个新函数,所以现在有了一个 "带参函数" 和剩下的 "无参函数" 。实际上,你从一个双参数函数开始,最后得到一个单参数函数。 - -您可以通过添加另一个级别来 curry 一个三参数函数: - -```java -// functional/Curry3Args.java - -import java.util.function.*; - -public class Curry3Args { - public static void main(String[] args) { - Function>> sum = - a -> b -> c -> a + b + c; - Function> hi = - sum.apply("Hi "); - Function ho = - hi.apply("Ho "); - System.out.println(ho.apply("Hup")); - } -} -/* Output: -Hi Ho Hup -*/ -``` - -对于每个级别的箭头级联(arrow-cascading),您可以围绕类型声明包装另一个函数。 - -处理原始类型和装箱时,请使用适当的功能接口: - -```java -// functional/CurriedIntAdd.java - -import java.util.function.*; - -public class CurriedIntAdd { - public static void main(String[] args) { - IntFunction - curriedIntAdd = a -> b -> a + b; - IntUnaryOperator add4 = curriedIntAdd.apply(4); - System.out.println(add4.applyAsInt(5)); - } -} -/* Output: -9 -*/ -``` - -您可以在因特网上找到更多 currying 示例。 通常这些是 Java 以外的语言,但如果你理解它们的基本概念,它们应该很容易翻译。 - - -## 纯函数式编程 - - -在没有函数支持的情况下,即使用像C这样的原始语言,也可以按照一定的原则编写纯函数程序。Java使它比这更容易,但是您必须小心地使一切都成为 **final**,并确保您的所有方法和函数没有副作用。因为 Java 本质上不是一种不可变的语言,所以如果您犯了错误,编译器不会提供任何帮助。 - -有第三方工具可以帮助您[^9],但是使用 **Scala** 或 **Clojure** 这样的语言可能更容易,因为它们从一开始就是为保持不变性而设计的。这些语言使您可以用 Java 编写项目的一部分,如果您必须用纯函数式编写,则可以用**Scala** 编写其他部分 (这需要一些规则) 或 **Clojure** (这需要的少得多)。虽然您将在并发编程一章中看到 Java确实支持并发,但是如果这是您项目的核心部分,您可能会考虑至少在项目的一部分中使用 **Scala** 或 **Clojure**之类的语言。 - - -## 本章小结 - - -Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。它们对 Java 是一个巨大的改进,因为它们允许您编写更简洁、更干净、更容易理解的代码。在下一章中,您将看到它们如何启用流。如果你像我一样,你会喜欢流媒体。 - -这些特性可能会满足大部分 Java 程序员的需求,他们已经对 Clojure 和 Scala 等新的、功能更强的语言感到不安和嫉妒,并阻止 Java 程序员流向这些语言 (或者,如果他们仍然决定迁移,至少会为他们做好更好的准备)。 - -但是,Lambdas 和方法引用远非完美,我们永远要为 Java 设计人员在早期令人兴奋的语言中做出的草率决定付出代价。特别是,没有泛型 lambda,所以 lambda 实际上不是 Java 中的第一类公民。这并不意味着 Java 8不是一个很大的改进,但它确实意味着,就像许多 Java 特性一样,您最终会感到沮丧。 - -当您遇到学习困难时,请记住,您可以从 ide (如 NetBeans、IntelliJ Idea 和 Eclipse )获得帮助,这些 ide 将建议您何时可以使用 lambda 表达式或方法引用 (并且经常为您重写代码!) - - - -1. 粘贴功能结合在一起是一个非常不同的方法,但它仍然使一种图书馆。 -2. 例如,这个电子书是利用 Pandoc 制作出来的,纯函数式语言编写的一个程序 Haskell。 -3. 有时函数语言将其描述为"代码即数据"。" -4. 这个语法来自 C++。 -5. 我还没有验证过这种说法。 -6. 在并发编程一章中,当您理解更改共享变量 "不是线程安全的" 时,这将更有意义。 -7. 接口能够支持方法的原因是它们是 Java 8 默认方法,您将在下一章中了解到。 -8. 一些语言,例如 Python,允许像调用其他函数一样调用组合函数。但这是 Java,所以我们取我们能得到的。 -9. 见,例如,不可变和可变性检测器。( Immutables and Mutability Detector) - - - - - - \ No newline at end of file diff --git a/book/14-Streams.md b/book/14-Streams.md deleted file mode 100644 index 5f8bd2fb..00000000 --- a/book/14-Streams.md +++ /dev/null @@ -1,32 +0,0 @@ -[TOC] - - -# 第十四章 流式编程 - - - -## 流支持 - - - -## 流创建 - - - -## 中级流操作 - - - -## Optional类 - - - -## 终端操作 - - - -## 本章小结 - - - - \ No newline at end of file diff --git a/book/15-Exceptions.md b/book/15-Exceptions.md deleted file mode 100644 index 564856de..00000000 --- a/book/15-Exceptions.md +++ /dev/null @@ -1,74 +0,0 @@ -[TOC] - - -# 第十五章 异常 - - - -## 异常概念 - - - -## 基本异常 - - - -## 异常捕获 - - - -## 自定义异常 - - - -## 异常规范 - - - -## 任意异常捕获 - - - -## Java标准异常 - - - -## finally关键字 - - - -## 异常限制 - - - -## 异常构造 - - - -## Try-With-Resources用法 - - - -## 异常匹配 - - - -## 异常准则 - - - -## 异常指南 - - - -## 本章小结 - - - - - - - - - - \ No newline at end of file diff --git a/book/16-Validating-Your-Code.md b/book/16-Validating-Your-Code.md deleted file mode 100644 index 2b296acc..00000000 --- a/book/16-Validating-Your-Code.md +++ /dev/null @@ -1,64 +0,0 @@ -[TOC] - - -# 第十六章 代码校验 - - - -## 测试 - - - -## 前提条件 - - - -## 测试驱动开发 - - - -## 日志 - - - -## 调试 - - - -## 基准测试 - - - -## 分析和优化 - - - -## 风格检测 - - - -## 静态错误分析 - - - -## 代码重审 - - - -## 结对编程 - - - -## 重构 - - - -## 持续集成 - - - -## 本章小结 - - - - \ No newline at end of file diff --git a/book/17-Files.md b/book/17-Files.md deleted file mode 100644 index 688d4bf8..00000000 --- a/book/17-Files.md +++ /dev/null @@ -1,37 +0,0 @@ -[TOC] - - -# 第十七章 文件 - - - -## 文件和目录路径 - - - -## 目录 - - - -## 文件系统 - - - -## 路径监听 - - - -## 文件查找 - - - -## 文件读写 - - - -## 本章小结 - - - - - \ No newline at end of file diff --git a/book/18-Strings.md b/book/18-Strings.md deleted file mode 100644 index fe2d7129..00000000 --- a/book/18-Strings.md +++ /dev/null @@ -1,50 +0,0 @@ -[TOC] - - -# 第十八章 字符串 - - - -## 字符串的不可变 - - - -## 重载和StringBuilder - - - -## 意外递归 - - - -## 字符串操作 - - - -## 格式化输出 - - - -## 常规表达式 - - - -## 扫描输入 - - - -## StringTokenizer类 - - - -## 本章小结 - - - - - - - - - - \ No newline at end of file diff --git a/book/19-Type-Information.md b/book/19-Type-Information.md deleted file mode 100644 index 8f38ac82..00000000 --- a/book/19-Type-Information.md +++ /dev/null @@ -1,49 +0,0 @@ -[TOC] - - -# 第十九章 类型信息 - - - -## 运行时类型信息 - - - -## 类的对象 - - - -## 类型转换检测 - - - -## 注册工厂 - - - -## 类的等价比较 - - - -## 反射运行时类信息 - - - -## 动态代理 - - - -## Optional类 - - - -## 接口和类型 - - - -## 本章小结 - - - - - \ No newline at end of file diff --git a/book/20-Generics.md b/book/20-Generics.md deleted file mode 100644 index 0ede2848..00000000 --- a/book/20-Generics.md +++ /dev/null @@ -1,86 +0,0 @@ -[TOC] - - -# 第二十章 泛型 - - - -## 简单泛型 - - - -## 泛型接口 - - - -## 泛型方法 - - - -## 复杂模型构建 - - - -## 泛型擦除 - - - -## 补偿擦除 - - - -## 边界 - - - -## 通配符 - - - -## 问题 - - - -## 自我约束类型 - - - -## 动态类型安全 - - - -## 泛型异常 - - - -## 混入 - - - -## 潜在类型 - - - -## 补偿不足 - - - -## 辅助潜在类型 - - - -## 泛型的优劣 - - - - - - - - - - - - - - \ No newline at end of file diff --git a/book/21-Arrays.md b/book/21-Arrays.md deleted file mode 100644 index a5abe15b..00000000 --- a/book/21-Arrays.md +++ /dev/null @@ -1,90 +0,0 @@ -[TOC] - - -# 第二十一章 数组 - - - -## 数组特性 - - - -## 一等对象 - - - -## 返回数组 - - - -## 多维数组 - - - -## 泛型数组 - - - -## Arrays的fill方法 - - - -## Arrays的setAll方法 - - - -## 增量生成 - - - -## 随机生成 - - - -## 泛型和基本数组 - - - -## 数组元素修改 - - - -## 数组并行 - - - -## Arrays工具类 - - - -## 数组拷贝 - - - -## 数组比较 - - - -## 流和数组 - - - -## 数组排序 - - - -## binarySearch二分查找 - - - -## parallelPrefix并行前缀 - - - -## 本章小结 - - - - - - \ No newline at end of file diff --git a/book/22-Enumerations.md b/book/22-Enumerations.md deleted file mode 100644 index 1456550b..00000000 --- a/book/22-Enumerations.md +++ /dev/null @@ -1,58 +0,0 @@ -[TOC] - - -# 第二十二章 枚举 - - - -## 基本功能 - - - -## 方法添加 - - - -## switch语句 - - - -## values方法 - - - -## 实现而非继承 - - - -## 随机选择 - - - -## 使用接口组织 - - - -## 使用EnumSet替代Flags - - - -## 使用EnumMap - - - -## 常量特定方法 - - - -## 多次调度 - - - -## 本章小结 - - - - - - \ No newline at end of file diff --git a/book/23-Annotations.md b/book/23-Annotations.md deleted file mode 100644 index 1f386a5c..00000000 --- a/book/23-Annotations.md +++ /dev/null @@ -1,30 +0,0 @@ -[TOC] - - -# 第二十三章 注解 - - - -## 基本语法 - - - -## 编写注解处理器 - - - -## 使用javac处理注解 - - - -## 基于注解的单元测试 - - - -## 本章小结 - - - - - - \ No newline at end of file diff --git a/book/24-Concurrent-Programming.md b/book/24-Concurrent-Programming.md deleted file mode 100644 index b52e5401..00000000 --- a/book/24-Concurrent-Programming.md +++ /dev/null @@ -1,66 +0,0 @@ -[TOC] - - -# 第二十四章 并发编程 - - - -## 术语问题 - - - -## 并发的超能力 - - - -## 针对速度 - - - -## 四句格言 - - - -## 残酷的真相 - - - -## 本章其余部分 - - - -## 并行流 - - - -## 创建和运行任务 - - - -## 终止耗时任务 - - - -## CompletableFuture类 - - - -## 死锁 - - - -## 构造函数非线程安全 - - - -## 复杂性和代价 - - - -## 本章小结 - - - - - - \ No newline at end of file diff --git a/book/25-Patterns.md b/book/25-Patterns.md deleted file mode 100644 index 9ae1f0d2..00000000 --- a/book/25-Patterns.md +++ /dev/null @@ -1,72 +0,0 @@ -[TOC] - - -# 第二十五章 设计模式 - - - -## 概念 - - - -## 构建型 - - - -## 面向实施 - - - -## 工厂模式 - - - -## 函数对象 - - - -## 接口改变 - - - -## 解释器 - - - -## 回调 - - - -## 多次调度 - - - -## 模式重构 - - - -## 抽象用法 - - - -## 多次派遣 - - - -## 访问者模式 - - - -## RTTI的优劣 - - - -## 本章小结 - - - - - - - - \ No newline at end of file diff --git a/book/Appendix-Becoming-a-Programmer.md b/book/Appendix-Becoming-a-Programmer.md deleted file mode 100644 index 5b7e4c21..00000000 --- a/book/Appendix-Becoming-a-Programmer.md +++ /dev/null @@ -1,32 +0,0 @@ -[TOC] - - -# 附录:成为一名程序员 - - - -## 如何开始 - - - -## 码农生涯 - - - -## 百分之五的神话 - - - -## 重在动手 - - - -## 像打字般编程 - - - -## 做你喜欢的事 - - - - \ No newline at end of file diff --git a/book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md b/book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md deleted file mode 100644 index 071a3c32..00000000 --- a/book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md +++ /dev/null @@ -1,29 +0,0 @@ -[TOC] - - -# 附录:静态语言类型检查 - - - -## 前言 - - - -## 静态类型检查和测试 - - - -## 如何提升打字 - - - -## 生产力的成本 - - - -## 静态和动态 - - - - - \ No newline at end of file diff --git a/book/Appendix-Collection-Topics.md b/book/Appendix-Collection-Topics.md deleted file mode 100644 index f479c555..00000000 --- a/book/Appendix-Collection-Topics.md +++ /dev/null @@ -1,78 +0,0 @@ -[TOC] - - -# 附录:集合主题 - - - -## 示例数据 - - - -## List表现 - - - -## Set表现 - - - -## 在Map中使用函数式操作 - - - -## 选择Map的部分 - - - -## 集合的fill方法 - - - -## 使用Flyweight自定义集合和Map - - - -## 集合功能 - - - -## 可选操作 - - - -## Set和存储顺序 - - - -## 队列 - - - -## 理解Map - - - -## 集合工具类 - - - -## 持有引用 - - - -## 避免旧式类库 - - - -## 本章小结 - - - - - - - - - - \ No newline at end of file diff --git a/book/Appendix-Data-Compression.md b/book/Appendix-Data-Compression.md deleted file mode 100644 index 10f4ea27..00000000 --- a/book/Appendix-Data-Compression.md +++ /dev/null @@ -1,20 +0,0 @@ -[TOC] - - -# 附录:数据压缩 - - - -## 使用Gzip简单压缩 - - - -## 使用zip多文件存储 - - - -## Java的jar - - - - \ No newline at end of file diff --git a/book/Appendix-IO-Streams.md b/book/Appendix-IO-Streams.md deleted file mode 100644 index 56c9342e..00000000 --- a/book/Appendix-IO-Streams.md +++ /dev/null @@ -1,37 +0,0 @@ -[TOC] - - -# 附录:流式IO - - - -## 输入流类型 - - - -## 输出流类型 - - - -## 添加属性和有用的接口 - - - -## Reader和Writer - - - -## RandomAccessFile类 - - - -## IO流典型用途 - - - -## 本章小结 - - - - - \ No newline at end of file diff --git a/book/Appendix-Javadoc.md b/book/Appendix-Javadoc.md deleted file mode 100644 index 5ea99544..00000000 --- a/book/Appendix-Javadoc.md +++ /dev/null @@ -1,8 +0,0 @@ -[TOC] - - -# 附录:文档注释 - - - - \ No newline at end of file diff --git a/book/Appendix-Low-Level-Concurrency.md b/book/Appendix-Low-Level-Concurrency.md deleted file mode 100644 index 9de1eb6b..00000000 --- a/book/Appendix-Low-Level-Concurrency.md +++ /dev/null @@ -1,42 +0,0 @@ -[TOC] - - -# 附录:并发底层原理 - - - -## 线程 - - - -## 异常捕获 - - - -## 资源共享 - - - -## volatile关键字 - - - -## 原子性 - - - -## 关键部分 - - - -## 库组件 - - - -## 本章小结 - - - - - - diff --git a/book/Appendix-New-IO.md b/book/Appendix-New-IO.md deleted file mode 100644 index da0dee11..00000000 --- a/book/Appendix-New-IO.md +++ /dev/null @@ -1,998 +0,0 @@ -[TOC] - - -# 附录:新IO - - -Java "新" I/O 库,是在Java 1.4引入到 **Java .nio.* package** 中。它有一个目标 : 速度。 - -实际上,"旧的"I/O包是使用 **nio** 重新实现了,因此速度也得到提升,因此即使不显式地使用nio 编写代码,也可以提速。这种速度的提高既发生在文件 I/O 中(这里讨论),也发生在网络I/O 中(例如用于Internet编程)。 - -速度来自于使用更接近操作系统执行 I/O 方式的结构 : 通道和缓冲区。把它想象成一个煤矿;通道是包含煤层的矿井(数据),缓冲区是您发送到矿井的小车。车里装满了煤,你从车上拿煤。也就是说,你不能直接和通道互动 ; 您与缓冲区交互并将缓冲区发送到通道中。通道要么从缓冲区中提取数据,要么将数据放入缓冲区。 - -本附录将深入探讨 nio 包。像I/O streams 这样的高级库使用 **nio**,但是大多数时候您不需要在这个级别使用 I/O 。在Java 7和Java 8,您(理想情况下)除了特殊情况外,甚至不需要使用I/O流。理想情况下,您将经常使用的所有内容都包含在文件一章中。只有在处理性能问题 (例如,可能需要内存映射文件)或创建自己的 I/O 库时,才需要理解 **nio**。 - - -## ByteBuffer - - -直接与通道通信的缓冲区只有一个类型,就是 bytebuffer 。也就是说,一个保存原始字节的缓冲区。如果您查看 **java.nio.ByteBuffer** 的JDK文档,你会发现它很基本 : 您可以通过告诉它要分配多少存储空间来创建一个方法,并且可以使用一些方法来放置和获取数据,这些方法可以是原始字节形式的数据,也可以是原始数据类型的数据。但是没有办法放置或获取对象,甚至字符串。它是相当低层次的,因为这使得大多数操作系统的映射更加有效。 - -"旧的" I/O 中的三个类被修改,可以生成一个 **FileChannel** 分别是: **FileInputStream**、**FileOutputStream**,以及用于读写的 **RandomAccessFile**。 - -注意,这些是字节操作流,符合 **nio** 的底层特性。 字符模式类的 **Reader** 和 **Writer** 不生成通道,但通道类具有从通道生成 **Reader** 和 **Writer** 的实用方法。 - -在这里,我们使用所有三种类型的流来生成可写、可读/可写和可读的通道: - -```java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -// Getting channels from streams -import java.nio.*; -import java.nio.channels.*; -import java.io.*; - -public class GetChannel { - private static String name = "data.txt"; - private static final int BSIZE = 1024; - public static void main(String[] args) { - // Write a file: - try( - FileChannel fc = new FileOutputStream(name) - .getChannel() - ) { - fc.write(ByteBuffer - .wrap("Some text ".getBytes())); - } catch(IOException e) { - throw new RuntimeException(e); - } - // Add to the end of the file: - try( - FileChannel fc = new RandomAccessFile( - name, "rw").getChannel() - ) { - fc.position(fc.size()); // Move to the end - fc.write(ByteBuffer - .wrap("Some more".getBytes())); - } catch(IOException e) { - throw new RuntimeException(e); - } - // Read the file: - try( - FileChannel fc = new FileInputStream(name) - .getChannel() - ) { - ByteBuffer buff = ByteBuffer.allocate(BSIZE); - fc.read(buff); - buff.flip(); - while(buff.hasRemaining()) - System.out.write(buff.get()); - } catch(IOException e) { - throw new RuntimeException(e); - } - System.out.flush(); - } -} -/* Output: -Some text Some more -*/ -``` - -对于这里显示的任何流类,**getChannel( )** 将生成一个 **FileChannel** 。通道是相当基本的:给它一个**ByteBuffer** 用于读写,并为独占访问锁定文件的区域(稍后将对此进行描述)。 - -将字节放入 **ByteBuffer** 的一种方法是直接使用 "put" 方法,将一个或多个字节填入 ByteBuffer,当然也可以是其它原始类型。但是,正如这里所看到的,您还可以使用 **wrap()** 方法在 **ByteBuffer**中"包装"现有字节数组。执行此操作时,不会复制底层数组,而是将其用作生成的 **ByteBuffer** 的存储。这样生产的 **ByteBuffer** 是由数组"支持"的。 - -data.txt 文件使用RandomAccessFile重新打开。注意,您可以在文件中移动 **FileChanne** l; 在这里,它被移动到末尾,以便添加额外的写操作。 - -对于只读访问,必须使用 **static allocate()** 方法显式地分配 **ByteBuffer**。**nio** 的目标是快速移动大量数据,因此 **ByteBuffer** 的大小应该很重要——实际上,这里使用的1K可能比您通常使用的要小很多(您必须对工作应用程序进行试验,以找到最佳大小)。 - -还可以通过使用 **allocateDirect()** 而不是 **allocation()** 来生成一个"直接"缓冲区来获得更快的速度,该缓冲区可以与操作系统进行更高的耦合。然而,这种分配的开销更大,而且实际实现因操作系统的不同而有所不同,因此,您必须再次试验您的工作应用程序,以发现直接缓冲区是否会为您带来速度上的优势。 - -一旦您调用 **read()** 来告诉 **FileChannel** 将字节存储到 **ByteBuffer** 中,您必须调用缓冲区上的 **flip()**来告诉它准备好提取字节(是的,这看起来有点粗糙,但是请记住,这是非常低级的,而且是为了达到最高速度)。如果我们要为进一步的 **read()** 操作使用缓冲区,我们还将调用 **clear()** 为每个 **read()** 准备缓冲区。这个简单的文件复制程序演示: - -```java -// newio/ChannelCopy.java - -// Copying a file using channels and buffers -// {java ChannelCopy ChannelCopy.java test.txt} -import java.nio.*; -import java.nio.channels.*; -import java.io.*; - -public class ChannelCopy { - private static final int BSIZE = 1024; - public static void main(String[] args) { - if(args.length != 2) { - System.out.println( - "arguments: sourcefile destfile"); - System.exit(1); - } - try( - FileChannel in = new FileInputStream( - args[0]).getChannel(); - FileChannel out = new FileOutputStream( - args[1]).getChannel() - ) { - ByteBuffer buffer = ByteBuffer.allocate(BSIZE); - while(in.read(buffer) != -1) { - buffer.flip(); // Prepare for writing - out.write(buffer); - buffer.clear(); // Prepare for reading - } - } catch(IOException e) { - throw new RuntimeException(e); - } - } -} - -``` - -打开一个 **FileChannel** 用于读取,另一个 **FileChannel** 用于写入。分配了一个 **ByteBuffer**,当**FileChannel.read() **返回 **-1** 时(毫无疑问,这是Unix和C语言中的一个剩余物 (holdover) ),意味着您已经完成了输入。每次 **read()** 将数据放入缓冲区之后,**flip()** 都会准备好缓冲区,以便 **write()** 提取它的信息。写()之后,信息仍然在缓冲区中,**clear()** 重置所有内部指针,以便在另一次 **read()** 期间接受数据。 - -但是,前面的程序并不是处理这种操作的理想方法。特殊方法 transferTo() 和 transferFrom( )允许你直接连接一个通道到另一个: - -```java -// newio/TransferTo.java - -// Using transferTo() between channels -// {java TransferTo TransferTo.java TransferTo.txt} -import java.nio.channels.*; -import java.io.*; - -public class TransferTo { - public static void main(String[] args) { - if(args.length != 2) { - System.out.println( - "arguments: sourcefile destfile"); - System.exit(1); - } - try( - FileChannel in = new FileInputStream( - args[0]).getChannel(); - FileChannel out = new FileOutputStream( - args[1]).getChannel() - ) { - in.transferTo(0, in.size(), out); - // Or: - // out.transferFrom(in, 0, in.size()); - } catch(IOException e) { - throw new RuntimeException(e); - } - } -} -``` - -你不会经常这样做,但知道这一点很好。 - - - -## 转换数据 - - -在 **GetChannel** 中打印文件中的信息。在 java 中,我们每次提取一个字节的数据,并将每个字节转换为 char。这看起来很简单——如果您查看**java.nio.CharBuffer** 类,您将看到它有一个toString()方法,该方法说,"返回一个包含这个缓冲区中的字符的字符串。" - -既然 **ByteBuffer** 可以用 **asCharBuffer()** 方法看作 **CharBuffer**,为什么不使用它呢? 从下面输出语句的第一行可以看出,这并不正确: - -```java -// newio/BufferToText.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -// Converting text to and from ByteBuffers -import java.nio.*; -import java.nio.channels.*; -import java.nio.charset.*; -import java.io.*; - -public class BufferToText { - private static final int BSIZE = 1024; - public static void main(String[] args) { - try( - FileChannel fc = new FileOutputStream( - "data2.txt").getChannel() - ) { - fc.write(ByteBuffer.wrap("Some text".getBytes())); - } catch(IOException e) { - throw new RuntimeException(e); - } - ByteBuffer buff = ByteBuffer.allocate(BSIZE); - try( - FileChannel fc = new FileInputStream( - "data2.txt").getChannel() - ) { - fc.read(buff); - } catch(IOException e) { - throw new RuntimeException(e); - } - buff.flip(); - // Doesn't work: - System.out.println(buff.asCharBuffer()); - // Decode using this system's default Charset: - buff.rewind(); - String encoding = - System.getProperty("file.encoding"); - System.out.println("Decoded using " + - encoding + ": " - + Charset.forName(encoding).decode(buff)); - // Encode with something that prints: - try( - FileChannel fc = new FileOutputStream( - "data2.txt").getChannel() - ) { - fc.write(ByteBuffer.wrap( - "Some text".getBytes("UTF-16BE"))); - } catch(IOException e) { - throw new RuntimeException(e); - } - // Now try reading again: - buff.clear(); - try( - FileChannel fc = new FileInputStream( - "data2.txt").getChannel() - ) { - fc.read(buff); - } catch(IOException e) { - throw new RuntimeException(e); - } - buff.flip(); - System.out.println(buff.asCharBuffer()); - // Use a CharBuffer to write through: - buff = ByteBuffer.allocate(24); - buff.asCharBuffer().put("Some text"); - try( - FileChannel fc = new FileOutputStream( - "data2.txt").getChannel() - ) { - fc.write(buff); - } catch(IOException e) { - throw new RuntimeException(e); - } - // Read and display: - buff.clear(); - try( - FileChannel fc = new FileInputStream( - "data2.txt").getChannel() - ) { - fc.read(buff); - } catch(IOException e) { - throw new RuntimeException(e); - } - buff.flip(); - System.out.println(buff.asCharBuffer()); - } -} -/* Output: -???? -Decoded using windows-1252: Some text -Some text -Some textNULNULNUL -*/ -``` - -缓冲区包含普通字节,为了将这些字节转换为字符,我们必须在输入时对它们进行编码(这样它们输出时就有意义了),或者在输出时对它们进行解码。 - -这可以使用 **java.nio.charset** 来完成。**Charset** 类,它提供工具来编码成许多不同类型的字符集: - -```java -// newio/AvailableCharSets.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -// Displays Charsets and aliases -import java.nio.charset.*; -import java.util.*; - -public class AvailableCharSets { - public static void main(String[] args) { - SortedMap charSets = - Charset.availableCharsets(); - for(String csName : charSets.keySet()) { - System.out.print(csName); - Iterator aliases = charSets.get(csName) - .aliases().iterator(); - if(aliases.hasNext()) - System.out.print(": "); - while(aliases.hasNext()) { - System.out.print(aliases.next()); - if(aliases.hasNext()) - System.out.print(", "); - } - System.out.println(); - } - } -} -/* Output: (First 7 Lines) -Big5: csBig5 -Big5-HKSCS: big5-hkscs, big5hk, Big5_HKSCS, big5hkscs -CESU-8: CESU8, csCESU-8 -EUC-JP: csEUCPkdFmtjapanese, x-euc-jp, eucjis, -Extended_UNIX_Code_Packed_Format_for_Japanese, euc_jp, -eucjp, x-eucjp -EUC-KR: ksc5601-1987, csEUCKR, ksc5601_1987, ksc5601, -5601, -euc_kr, ksc_5601, ks_c_5601-1987, euckr -GB18030: gb18030-2000 -GB2312: gb2312, euc-cn, x-EUC-CN, euccn, EUC_CN, -gb2312-80, -gb2312-1980 - ... -*/ -``` - -回到 **BufferToText** 。java 中,如果您 **rewind()** 缓冲区(回到数据的开头),然后使用该平台的默认字符集 **decode()** 数据,生成的 **CharBuffer** 将在控制台上正常显示。要发现默认字符集,使用**System.getProperty("file.encoding")**,它生成命名字符集的字符串。 - -另一种方法是使用字符集 **encode()**,该字符集在读取文件时生成可打印的内容,如您在**BufferToText.java** 的第三部分中所看到的。这里,**UTF-16BE** 用于将文本写入文件,当读取文本时,您所要做的就是将其转换为 **CharBuffer**,并生成预期的文本。最后,您将看到如果通过**CharBuffer** 写入 **ByteBuffer** 会发生什么(稍后您将对此有更多的了解)。注意,为 **ByteBuffer** 分配了24个字节。 - -由于每个字符需要两个字节,这对于12个字符已经足够了,但是"some text"只有9个字节。其余的零字节仍然出现在由其toString()生成的CharBuffer的表示中,如输出所示。 - - - -## 获取原始类型 - - -虽然 **ByteBuffer** 只包含字节,但它包含了一些方法,用于从它所包含的字节中生成每种不同类型的基元值。这个例子展示了使用以下方法插入和提取各种值: - -```java -// newio/GetData.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -// Getting different representations from a ByteBuffer -import java.nio.*; - -public class GetData { - private static final int BSIZE = 1024; - public static void main(String[] args) { - ByteBuffer bb = ByteBuffer.allocate(BSIZE); - // Allocation automatically zeroes the ByteBuffer: - int i = 0; - while(i++ < bb.limit()) - if(bb.get() != 0) - System.out.println("nonzero"); - System.out.println("i = " + i); - bb.rewind(); - // Store and read a char array: - bb.asCharBuffer().put("Howdy!"); - char c; - while((c = bb.getChar()) != 0) - System.out.print(c + " "); - System.out.println(); - bb.rewind(); - // Store and read a short: - bb.asShortBuffer().put((short)471142); - System.out.println(bb.getShort()); - bb.rewind(); - // Store and read an int: - bb.asIntBuffer().put(99471142); - System.out.println(bb.getInt()); - bb.rewind(); - // Store and read a long: - bb.asLongBuffer().put(99471142); - System.out.println(bb.getLong()); - bb.rewind(); - // Store and read a float: - bb.asFloatBuffer().put(99471142); - System.out.println(bb.getFloat()); - bb.rewind(); - // Store and read a double: - bb.asDoubleBuffer().put(99471142); - System.out.println(bb.getDouble()); - bb.rewind(); - } -} -/* Output: -i = 1025 -H o w d y ! -12390 -99471142 -99471142 -9.9471144E7 -9.9471142E7 -*/ -``` - -在分配 **ByteBuffer** 之后,将检查它的值,以确定缓冲区分配是否会自动将内容归零——它确实会这样做。检查所有1,024个值(直到 position的值为 缓冲区的 **limit()** ),所有值都为零。 - -将原始值插入ByteBuffer的最简单方法是使用 **asCharBuffer()** 、**asShortBuffer()** 等获取该缓冲区的适当"视图",然后使用该视图的 **put()** 方法。 - - -这将对每个基本数据类型执行。其中唯一有点奇怪的是 **ShortBuffer** 的 **put()**,它需要强制转换 (强制转换并更改结果值)。所有其他视图缓冲区都不需要在它们的 **put()** 方法中强制转换。 - - -## 视图缓冲区 - - -"视图缓冲区"通过特定原始类型的窗口来查看底层 **ByteBuffer**。**ByteBuffer** 仍然是"支持"视图的实际存储,因此对视图所做的任何更改都反映在对 **ByteBuffer** 中的数据的修改中。 - -如前面的示例所示,这方便地将基本类型插入 **ByteBuffer**。视图缓冲区还可以从 **ByteBuffer** 读取原始值,一次读取一个(ByteBuffer允许),或者批量读取(数组)。下面是一个通过 **IntBuffer**在**ByteBuffer** 中操作 int 的例子: - -```java -// newio/IntBufferDemo.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -// Manipulating ints in a ByteBuffer with an IntBuffer -import java.nio.*; - -public class IntBufferDemo { - private static final int BSIZE = 1024; - public static void main(String[] args) { - ByteBuffer bb = ByteBuffer.allocate(BSIZE); - IntBuffer ib = bb.asIntBuffer(); - // Store an array of int: - ib.put(new int[]{ 11, 42, 47, 99, 143, 811, 1016 }); - // Absolute location read and write: - System.out.println(ib.get(3)); - ib.put(3, 1811); - // Setting a new limit before rewinding the buffer. - ib.flip(); - while(ib.hasRemaining()) { - int i = ib.get(); - System.out.println(i); - } - } -} -/* Output: -99 -11 -42 -47 -1811 -143 -811 -1016 -*/ -``` - -重载的 **put()** 方法首先用于存储 **int** 数组。下面的 **get()** 和 **put()** 方法调用直接访问底层 **ByteBuffer** 中的 **int** 位置。请注意,通过直接操作 **ByteBuffer** ,这些绝对位置访问也可以用于基本类型。 - -一旦底层 **ByteBuffer** 通过视图缓冲区填充了 **int** 或其他基本类型,那么就可以直接将该 **ByteBuffer **写入通道。您可以轻松地从通道读取数据,并使用视图缓冲区将所有内容转换为特定类型的原语。下面是一个例子,通过在同一个 **ByteBuffer** 上生成不同的视图缓冲区,将相同的字节序列解释为 **short**、**int**、**float**、**long **和 **double**: - -```java -// newio/ViewBuffers.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -import java.nio.*; - -public class ViewBuffers { - public static void main(String[] args) { - ByteBuffer bb = ByteBuffer.wrap( - new byte[]{ 0, 0, 0, 0, 0, 0, 0, 'a' }); - bb.rewind(); - System.out.print("Byte Buffer "); - while(bb.hasRemaining()) - System.out.print( - bb.position()+ " -> " + bb.get() + ", "); - System.out.println(); - CharBuffer cb = - ((ByteBuffer)bb.rewind()).asCharBuffer(); - System.out.print("Char Buffer "); - while(cb.hasRemaining()) - System.out.print( - cb.position() + " -> " + cb.get() + ", "); - System.out.println(); - FloatBuffer fb = - ((ByteBuffer)bb.rewind()).asFloatBuffer(); - System.out.print("Float Buffer "); - while(fb.hasRemaining()) - System.out.print( - fb.position()+ " -> " + fb.get() + ", "); - System.out.println(); - IntBuffer ib = - ((ByteBuffer)bb.rewind()).asIntBuffer(); - System.out.print("Int Buffer "); - while(ib.hasRemaining()) - System.out.print( - ib.position()+ " -> " + ib.get() + ", "); - System.out.println(); - LongBuffer lb = - ((ByteBuffer)bb.rewind()).asLongBuffer(); - System.out.print("Long Buffer "); - while(lb.hasRemaining()) - System.out.print( - lb.position()+ " -> " + lb.get() + ", "); - System.out.println(); - ShortBuffer sb = - ((ByteBuffer)bb.rewind()).asShortBuffer(); - System.out.print("Short Buffer "); - while(sb.hasRemaining()) - System.out.print( - sb.position()+ " -> " + sb.get() + ", "); - System.out.println(); - DoubleBuffer db = - ((ByteBuffer)bb.rewind()).asDoubleBuffer(); - System.out.print("Double Buffer "); - while(db.hasRemaining()) - System.out.print( - db.position()+ " -> " + db.get() + ", "); - } -} -/* Output: -Byte Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 0, 4 -> 0, 5 --> 0, 6 -> 0, 7 -> 97, -Char Buffer 0 -> NUL, 1 -> NUL, 2 -> NUL, 3 -> a, -Float Buffer 0 -> 0.0, 1 -> 1.36E-43, -Int Buffer 0 -> 0, 1 -> 97, -Long Buffer 0 -> 97, -Short Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 97, -Double Buffer 0 -> 4.8E-322, -*/ -``` - -**ByteBuffer** 通过"包装"一个8字节数组生成,然后通过所有不同基本类型的视图缓冲区显示该数组。下图显示了从不同类型的缓冲区读取数据时,数据显示的差异: - -![image-20190324153222402](/Users/langdon/Library/Application Support/typora-user-images/image-20190324153222402.png) - - - -### 端 - -不同的机器可以使用不同的字节顺序方法来存储数据。"Big endian"将最重要的字节(the most significant byte)放在最低内存地址中,而"little endian"将最重要的字节放在最高内存地址中。 - -当存储一个大于一个字节的量时,例如 **int**、**float** 等,您可能需要考虑字节排序。字节缓冲区以大端字节形式存储数据,通过网络发送的数据总是使用大端字节顺序。您可以使用 **order()** 和**ByteOrder** 参数来更改 **ByteBuffer** 端,可选参数只有2个:**ByteOrder.BIG_ENDIAN** 或 **ByteOrder.LITTLE_ENDIAN**。 - -考虑一个包含以下两个字节的 **ByteBuffer** :将数据作为一个**short** (**ByteBuffer. asshortbuffer()**) 读取,生成数字 97 (00000000 01100001)。更改为 little endian 将生成数字 24832 (01100001 00000000)。 - -这显示了字节顺序的变化取决于 endian 设置: - -```java -// newio/Endians.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -// Endian differences and data storage -import java.nio.*; -import java.util.*; - -public class Endians { - public static void main(String[] args) { - ByteBuffer bb = ByteBuffer.wrap(new byte[12]); - bb.asCharBuffer().put("abcdef"); - System.out.println(Arrays.toString(bb.array())); - bb.rewind(); - bb.order(ByteOrder.BIG_ENDIAN); - bb.asCharBuffer().put("abcdef"); - System.out.println(Arrays.toString(bb.array())); - bb.rewind(); - bb.order(ByteOrder.LITTLE_ENDIAN); - bb.asCharBuffer().put("abcdef"); - System.out.println(Arrays.toString(bb.array())); - } -} -/* Output: -[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102] -[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102] -[97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0] -*/ - -``` - -**ByteBuffer** 分配空间将 **charArray** 中的所有字节作为外部缓冲区保存,因此可以调用 **array()** 方法来显示底层字节。**array()** 方法是"可选的",您只能在数组支持的缓冲区上调用它 ,否则您将得到一个 **UnsupportedOperationException**。 - -**charArray** 通过 **CharBuffer** 视图插入到 **ByteBuffer **中。当显示底层字节时,默认顺序与随后的大端序相同,而小端序则交换字节。 - - -## 使用缓冲区进行数据操作 - - -下图说明了 nio 类之间的关系,展示了如何移动和转换数据。例如,要将字节数组写入文件,使用ByteBuffer.wrap() 方法包装字节数组,使用 **getChannel()** 在 **FileOutputStream** 上打开通道,然后从 **ByteBuffer** 将数据写入 **FileChannel**。 - -![image-20190324153202297](/Users/langdon/Library/Application Support/typora-user-images/image-20190324153202297.png) - -**ByteBuffer** 是将数据移入和移出通道的唯一方法,您只能创建一个独立的原始类型的缓冲区,或者使用"as"方法从ByteBuffer获得一个新缓冲区。也就是说,不能将基元类型的缓冲区转换为ByteBuffer。但您能够通过视图缓冲区将原始数据移动到 **ByteBuffer** 中或移出 **ByteBuffer**。 - - - -### 缓冲区的细节 - -缓冲区由数据和四个索引组成,以有效地访问和操作该数据: 标记、位置、限制 和 容量(mark, position, limit and capacity)。有一些方法可以设置和重置这些索引并查询它们的值。 - -**capacity()** 返回缓冲区的 capacity。 - -**clear()** 清除缓冲区,将 position 设置为零并 设 limit 为 capacity。您可以调用此方法来覆盖现有缓冲区。 - -**flip()** 将 limit 设置为位置,并将 position 设置为零。此方法用于准备缓冲区,以便在数据写入缓冲区后进行读取。 - -**limit()** 返回 limit 的值。 - -**limit(int lim)** 重设 limit - -**mark()** 设置 mark 为当前的 position - -**position()** 返回 position - -**position(int pos) ** 设置 position - -**remaining()** 返回 limit - position 。 - -**hasRemaining()** 如果在 position 与 limit 中间有元素,返回 **true** - -从缓冲区插入和提取数据的方法更新这些索引来反映所做的更改。这个例子使用了一个非常简单的算法(交换相邻的字符)来打乱和整理字符在CharBuffer: - -```java -// newio/UsingBuffers.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -import java.nio.*; - -public class UsingBuffers { - private static - void symmetricScramble(CharBuffer buffer) { - while(buffer.hasRemaining()) { - buffer.mark(); - char c1 = buffer.get(); - char c2 = buffer.get(); - buffer.reset(); - buffer.put(c2).put(c1); - } - } - public static void main(String[] args) { - char[] data = "UsingBuffers".toCharArray(); - ByteBuffer bb = - ByteBuffer.allocate(data.length * 2); - CharBuffer cb = bb.asCharBuffer(); - cb.put(data); - System.out.println(cb.rewind()); - symmetricScramble(cb); - System.out.println(cb.rewind()); - symmetricScramble(cb); - System.out.println(cb.rewind()); - } -} -/* Output: -UsingBuffers -sUniBgfuefsr -UsingBuffers -*/ -``` - -虽然可以通过使用 **char** 数组调用 **wrap()** 直接生成 **CharBuffer**,但是底层的 **ByteBuffer** 将被分配,而 **CharBuffer** 将作为 **ByteBuffer** 上的视图生成。这强调了目标始终是操作 **ByteBuffer**,因为它与通道交互。 - -下面是程序在 **symmetricgrab()** 方法入口时缓冲区的样子: - -![image-20190324155153600](/Users/langdon/Library/Application Support/typora-user-images/image-20190324155153600.png) - -position 指向缓冲区中的第一个元素,capacity 和 limie 紧接在最后一个元素之后。在**symmetricgrab()** 中,while循环迭代到 position 等于 limit。当在缓冲区上调用相对位置的 get() 或 put() 函数时,缓冲区的位置会发生变化。您还可以调用绝对位置的 get() 和 put() 方法,它们包含索引参数 : get()或put()发生的位置。这些方法不修改缓冲区 position 的值。 - -当控件进入while循环时,使用 **mark()** 调用设置 mark 的值。缓冲区的状态为: - -两个相对 get() 调用将前两个字符的值保存在变量c1和c2中。在这两个调用之后,缓冲区看起来是这样的 : 为了执行交换,我们在位置0处编写c2,在位置1处编写c1。我们可以使用绝对put方法来实现这一点,或者用 reset() 方法,将 position 的值设置为 mark 。 - -两个put()方法分别编写c2和c1 : 在循环的下一次迭代中,将mark设置为position的当前值 : 该过程将继续,直到遍历整个缓冲区为止。在while循环的末尾,position位于缓冲区的末尾。如果显示缓冲区,则只显示位置和限制之间的字符。因此,要显示缓冲区的全部内容,必须使用 rewind() 将位置设置为缓冲区的开始位置。这是 rewind() 调用后 buffer 的状态(mark的值变成undefined): - -![image-20190324155528149](/Users/langdon/Library/Application Support/typora-user-images/image-20190324155528149.png) - -再次调用 **symmetricgrab()** 函数时,**CharBuffer** 将经历相同的过程并恢复到原始状态。 - - -## 内存映射文件 - - -内存映射文件允许您创建和修改太大而无法放入内存的文件。使用内存映射文件,您可以假装整个文件都在内存中,并将其视为一个非常大的数组来访问它。这种方法大大简化了您编写的修改文件的代码: - -```java -// newio/LargeMappedFiles.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -// Creating a very large file using mapping -import java.nio.*; -import java.nio.channels.*; -import java.io.*; - -public class LargeMappedFiles { - static int length = 0x8000000; // 128 MB - public static void - main(String[] args) throws Exception { - try( - RandomAccessFile tdat = - new RandomAccessFile("test.dat", "rw") - ) { - MappedByteBuffer out = tdat.getChannel().map( - FileChannel.MapMode.READ_WRITE, 0, length); - for(int i = 0; i < length; i++) - out.put((byte)'x'); - System.out.println("Finished writing"); - for(int i = length/2; i < length/2 + 6; i++) - System.out.print((char)out.get(i)); - } - } -} -/* Output: -Finished writing -xxxxxx -*/ -``` - -为了读写,我们从 **RandomAccessFile** 开始,获取该文件的通道,然后调用 **map()** 来生成**MappedByteBuffer** ,这是一种特殊的直接缓冲区。您必须指定要在文件中映射的区域的起始点和长度—这意味着您可以选择映射大文件的较小区域。**MappedByteBuffer** 继承了它的**ByteBuffer**,所以它有所有的 **ByteBuffer **方法。这里只展示了 **put()** 和 **get()** 的最简单用法,但是您也可以使用 **asCharBuffer()** 等方法。 - -使用前面的程序创建的文件有128mb长,可能比您的操作系统一次允许的内存要大。该文件似乎可以同时访问,因为它只有一部分被带进内存,而其他部分被交换出去。这样,一个非常大的文件(最多2gb)可以很容易地修改。注意,底层操作系统的文件映射工具用于最大化性能。 - -### 性能 - -虽然"旧"流I/O的性能通过使用 **nio** 实现得到了改进,但是映射文件访问往往要快得多。这个程序做一个简单的性能比较: - -```java -// newio/MappedIO.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -import java.util.*; -import java.nio.*; -import java.nio.channels.*; -import java.io.*; - -public class MappedIO { - private static int numOfInts = 4_000_000; - private static int numOfUbuffInts = 100_000; - private abstract static class Tester { - private String name; - Tester(String name) { - this.name = name; - } - public void runTest() { - System.out.print(name + ": "); - long start = System.nanoTime(); - test(); - double duration = System.nanoTime() - start; - System.out.format("%.3f%n", duration/1.0e9); - } - public abstract void test(); - } - private static Tester[] tests = { - new Tester("Stream Write") { - @Override - public void test() { - try( - DataOutputStream dos = - new DataOutputStream( - new BufferedOutputStream( - new FileOutputStream( - new File("temp.tmp")))) - ) { - for(int i = 0; i < numOfInts; i++) - dos.writeInt(i); - } catch(IOException e) { - throw new RuntimeException(e); - } - } - }, - new Tester("Mapped Write") { - @Override - public void test() { - try( - FileChannel fc = - new RandomAccessFile("temp.tmp", "rw") - .getChannel() - ) { - IntBuffer ib = - fc.map(FileChannel.MapMode.READ_WRITE, - 0, fc.size()).asIntBuffer(); - for(int i = 0; i < numOfInts; i++) - ib.put(i); - } catch(IOException e) { - throw new RuntimeException(e); - } - } - }, - new Tester("Stream Read") { - @Override - public void test() { - try( - DataInputStream dis = - new DataInputStream( - new BufferedInputStream( - new FileInputStream("temp.tmp"))) - ) { - for(int i = 0; i < numOfInts; i++) - dis.readInt(); - } catch(IOException e) { - throw new RuntimeException(e); - } - } - }, - new Tester("Mapped Read") { - @Override - public void test() { - try( - FileChannel fc = new FileInputStream( - new File("temp.tmp")).getChannel() - ) { - IntBuffer ib = - fc.map(FileChannel.MapMode.READ_ONLY, - 0, fc.size()).asIntBuffer(); - while(ib.hasRemaining()) - ib.get(); - } catch(IOException e) { - throw new RuntimeException(e); - } - } - }, - new Tester("Stream Read/Write") { - @Override - public void test() { - try( - RandomAccessFile raf = - new RandomAccessFile( - new File("temp.tmp"), "rw") - ) { - raf.writeInt(1); - for(int i = 0; i < numOfUbuffInts; i++) { - raf.seek(raf.length() - 4); - raf.writeInt(raf.readInt()); - } - } catch(IOException e) { - throw new RuntimeException(e); - } - } - }, - new Tester("Mapped Read/Write") { - @Override - public void test() { - try( - FileChannel fc = new RandomAccessFile( - new File("temp.tmp"), "rw").getChannel() - ) { - IntBuffer ib = - fc.map(FileChannel.MapMode.READ_WRITE, - 0, fc.size()).asIntBuffer(); - ib.put(0); - for(int i = 1; i < numOfUbuffInts; i++) - ib.put(ib.get(i - 1)); - } catch(IOException e) { - throw new RuntimeException(e); - } - } - } - }; - public static void main(String[] args) { - Arrays.stream(tests).forEach(Tester::runTest); - } -} -/* Output: -Stream Write: 0.615 -Mapped Write: 0.050 -Stream Read: 0.577 -Mapped Read: 0.015 -Stream Read/Write: 4.069 -Mapped Read/Write: 0.013 -*/ -``` - -**Tester** 是一个模板方法模式,它为匿名内部子类中定义的 **test()** 的各种实现创建一个测试框架。每个子类都执行一种测试,因此 test() 方法还为您提供了执行各种I/O活动的原型。 - -虽然映射的写似乎使用 **FileOutputStream**,但是文件映射中的所有输出必须使用 **RandomAccessFile**,就像前面代码中的读/写一样。 - -请注意,**test()** 方法包括初始化各种I/O对象的时间,因此,尽管映射文件的设置可能很昂贵,但是与流I/O相比,总体收益非常可观。 - - -## 文件锁定 - - -文件锁定同步访问,因此文件可以是共享资源。但是,争用同一个文件的两个线程可能位于不同的jvm 中,可能一个是 Java 线程,另一个是操作系统中的某个本机线程。文件锁定对其他操作系统进程是可见的,因为Java文件锁定直接映射到本机操作系统锁定工具。 - -```java -// newio/FileLocking.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -import java.nio.channels.*; -import java.util.concurrent.*; -import java.io.*; - -public class FileLocking { - public static void main(String[] args) { - try( - FileOutputStream fos = - new FileOutputStream("file.txt"); - FileLock fl = fos.getChannel().tryLock() - ) { - if(fl != null) { - System.out.println("Locked File"); - TimeUnit.MILLISECONDS.sleep(100); - fl.release(); - System.out.println("Released Lock"); - } - } catch(IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } -} -/* Output: -Locked File -Released Lock -*/ -``` - -通过调用 **FileChannel** 上的 **tryLock()** 或 **lock()**,可以获得整个文件的 **FileLock**。( **SocketChannel、DatagramChannel **和 **ServerSocketChannel **不需要锁定,因为它们本质上是单进程实体 ; 通常不会在两个进程之间共享一个网络套接字) **tryLock()** 是非阻塞的。它试图抓住锁,但如果它不能抓住了(当其他进程已经持有相同的锁,并且它不是共享的),它只是从方法调用返回。 - -**lock()** 会阻塞,直到获得锁,或者调用 **lock()** 的线程中断,或者调用 **lock()** 方法的通道关闭。使 用**FileLock.release()** 释放锁。还可以使用 **tryLock(long position, long size, boolean shared)** 或**lock(long position, long size, boolean shared)** 锁定文件的一部分,锁住该区域(size-position)。第三个参数指定是否共享此锁。 - -虽然零参数锁定方法适应文件大小的变化,但是如果文件大小发生变化,具有固定大小的锁不会发生变化。如果从一个位置到另一个位置获得一个锁,并且文件的增长超过了 position + size ,那么超出位置+大小的部分没有被锁定。零参数锁定方法锁定整个文件,即使它在增长。 - -底层操作系统必须提供对独占锁或共享锁的支持。如果操作系统不支持共享锁,并且请求共享锁,则使用独占锁。可以使用 **FileLock.isShared()** 查询锁的类型 (共享或独占)。 - - - -### 锁定映射文件的某些部分 - -文件映射通常用于非常大的文件。您可能需要锁定此类文件的某些部分,以便其他进程可以修改未锁定的部分。例如,数据库必须同时对许多用户可用。这里你可以看到两个线程,每个线程都锁定文件的不同部分: - -```java -// newio/LockingMappedFiles.java -// (c)2017 MindView LLC: see Copyright.txt -// We make no guarantees that this code is fit for any purpose. -// Visit http://OnJava8.com for more book information. -// Locking portions of a mapped file -import java.nio.*; -import java.nio.channels.*; -import java.io.*; - -public class LockingMappedFiles { - static final int LENGTH = 0x8FFFFFF; // 128 MB - static FileChannel fc; - public static void - main(String[] args) throws Exception { - fc = new RandomAccessFile("test.dat", "rw") - .getChannel(); - MappedByteBuffer out = fc.map( - FileChannel.MapMode.READ_WRITE, 0, LENGTH); - for(int i = 0; i < LENGTH; i++) - out.put((byte)'x'); - new LockAndModify(out, 0, 0 + LENGTH/3); - new LockAndModify( - out, LENGTH/2, LENGTH/2 + LENGTH/4); - } - private static class LockAndModify extends Thread { - private ByteBuffer buff; - private int start, end; - LockAndModify(ByteBuffer mbb, int start, int end) { - this.start = start; - this.end = end; - mbb.limit(end); - mbb.position(start); - buff = mbb.slice(); - start(); - } - @Override - public void run() { - try { - // Exclusive lock with no overlap: - FileLock fl = fc.lock(start, end, false); - System.out.println( - "Locked: "+ start +" to "+ end); - // Perform modification: - while(buff.position() < buff.limit() - 1) - buff.put((byte)(buff.get() + 1)); - fl.release(); - System.out.println( - "Released: " + start + " to " + end); - } catch(IOException e) { - throw new RuntimeException(e); - } - } - } -} -/* Output: -Locked: 75497471 to 113246206 -Locked: 0 to 50331647 -Released: 75497471 to 113246206 -Released: 0 to 50331647 -*/ - -``` - -**LockAndModify** 线程类设置缓冲区并创建要修改的 **slice()**,在 **run()** 中,锁在文件通道上获取(不能在缓冲区上获取锁—只能在通道上获取锁)。**lock()** 的调用非常类似于获取对象上的线程锁——现在有了一个"临界区",可以对文件的这部分进行独占访问。[^1] - -当 JVM 退出或关闭获取锁的通道时,锁会自动释放,但是您也可以显式地调用 **FileLock** 对象上的**release()**,如上所示。 - - - -1. 您可以在附录:低级并发中找到关于线程的更多细节。 -======= - - - diff --git a/book/Appendix-Object-Serialization.md b/book/Appendix-Object-Serialization.md deleted file mode 100644 index 646dd1b2..00000000 --- a/book/Appendix-Object-Serialization.md +++ /dev/null @@ -1,22 +0,0 @@ -[TOC] - - -# 附录:对象序列化 - - - -## 查找类 - - - -## 控制序列化 - - - -## 使用持久化 - - - - - - \ No newline at end of file diff --git a/book/Appendix-Passing-and-Returning-Objects.md b/book/Appendix-Passing-and-Returning-Objects.md deleted file mode 100644 index de6d712e..00000000 --- a/book/Appendix-Passing-and-Returning-Objects.md +++ /dev/null @@ -1,30 +0,0 @@ -[TOC] - - -# 附录:对象传递和返回 - - - -## 传递引用 - - - -## 本地拷贝 - - - -## 控制克隆 - - - -## 不可变类 - - - -## 本章小结 - - - - - - \ No newline at end of file diff --git a/book/Appendix-Programming-Guidelines.md b/book/Appendix-Programming-Guidelines.md deleted file mode 100644 index 2d654a13..00000000 --- a/book/Appendix-Programming-Guidelines.md +++ /dev/null @@ -1,18 +0,0 @@ -[TOC] - - -# 附录:编程指南 - - - -## 设计 - - - -## 实现 - - - - - - \ No newline at end of file diff --git a/book/Appendix-Standard-IO.md b/book/Appendix-Standard-IO.md deleted file mode 100644 index eab20ad3..00000000 --- a/book/Appendix-Standard-IO.md +++ /dev/null @@ -1,11 +0,0 @@ -[TOC] - - -# 附录:标准IO - - -## 执行控制 - - - - \ No newline at end of file diff --git a/book/Appendix-Supplements.md b/book/Appendix-Supplements.md deleted file mode 100644 index e5cc7917..00000000 --- a/book/Appendix-Supplements.md +++ /dev/null @@ -1,20 +0,0 @@ -[TOC] - - -# 附录:补充 - - - -## 可下载的补充 - - - -## 通过Thinking-in-C来巩固Java基础 - - - -## 动手实践 - - - - \ No newline at end of file diff --git a/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md b/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md deleted file mode 100644 index fb26815b..00000000 --- a/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md +++ /dev/null @@ -1,8 +0,0 @@ -[TOC] - - -# 附录:C++和Java的优良传统 - - - - diff --git a/book/Appendix-Understanding-equals-and-hashCode.md b/book/Appendix-Understanding-equals-and-hashCode.md deleted file mode 100644 index 1dafbdcf..00000000 --- a/book/Appendix-Understanding-equals-and-hashCode.md +++ /dev/null @@ -1,19 +0,0 @@ -[TOC] - - -# 附录:理解equals和hashCode方法 - - - -## equals典范 - - - -## 哈希和哈希码 - - - -## 调整HashMap - - - \ No newline at end of file diff --git a/cover.jpg b/cover.jpg deleted file mode 100644 index 1819c274..00000000 Binary files a/cover.jpg and /dev/null differ diff --git a/cover_small.jpg b/cover_small.jpg deleted file mode 100644 index d896e8a1..00000000 Binary files a/cover_small.jpg and /dev/null differ diff --git a/images/1545758268350.png b/images/1545758268350.png deleted file mode 100644 index 298d67fd..00000000 Binary files a/images/1545758268350.png and /dev/null differ diff --git a/images/1545763399825.png b/images/1545763399825.png deleted file mode 100644 index 09d00f31..00000000 Binary files a/images/1545763399825.png and /dev/null differ diff --git a/images/1545764724202.png b/images/1545764724202.png deleted file mode 100644 index dc738e42..00000000 Binary files a/images/1545764724202.png and /dev/null differ diff --git a/images/1545764780795.png b/images/1545764780795.png deleted file mode 100644 index eafc3fb8..00000000 Binary files a/images/1545764780795.png and /dev/null differ diff --git a/images/1545764820176.png b/images/1545764820176.png deleted file mode 100644 index e58ac3e4..00000000 Binary files a/images/1545764820176.png and /dev/null differ diff --git a/images/1545839316314.png b/images/1545839316314.png deleted file mode 100644 index e384c734..00000000 Binary files a/images/1545839316314.png and /dev/null differ diff --git a/images/1545841270997.png b/images/1545841270997.png deleted file mode 100644 index a3e5e053..00000000 Binary files a/images/1545841270997.png and /dev/null differ diff --git a/images/QQGroupQRCode.png b/images/QQGroupQRCode.png deleted file mode 100644 index 05d6ae40..00000000 Binary files a/images/QQGroupQRCode.png and /dev/null differ diff --git a/images/cover.jpg b/images/cover.jpg deleted file mode 100644 index 1819c274..00000000 Binary files a/images/cover.jpg and /dev/null differ diff --git a/images/cover_small.jpg b/images/cover_small.jpg deleted file mode 100644 index d896e8a1..00000000 Binary files a/images/cover_small.jpg and /dev/null differ diff --git a/images/level_1_title.png b/images/level_1_title.png deleted file mode 100644 index 620f42b4..00000000 Binary files a/images/level_1_title.png and /dev/null differ diff --git a/images/level_2_title.png b/images/level_2_title.png deleted file mode 100644 index 6869a8b6..00000000 Binary files a/images/level_2_title.png and /dev/null differ diff --git a/images/qqgroup.png b/images/qqgroup.png deleted file mode 100644 index 92c44842..00000000 Binary files a/images/qqgroup.png and /dev/null differ diff --git a/images/reader.png b/images/reader.png deleted file mode 100644 index 7190719c..00000000 Binary files a/images/reader.png and /dev/null differ

AltStyle によって変換されたページ (->オリジナル) /