Java 编程思想 (Thinking in Java) 通篇阅读笔记
书籍基本信息:
书名: Java编程思想(第四版)
作者: Bruce Eckel
版次: 2007年9月第1版第3次印刷
ISBN: 978-7-111-21382-6
目录之前所有内容:
- 本书的一个重点是有关
集合
的概念 - 本书有关于Java语言设计原因以及运行方式以及出现问题相关内容
- 本书基于
JDK5.0
作为Java语言的基础学习书籍. 而且由于本书没有覆盖所有的Java类, 所以学习的核心是设计思想以及实现思路 - 编程所追求的目标是:
写出健壮的, 高效的, 灵活的程序
- Java相比于C++的核心差别是
Java的设计目标是专注于攻克开发与维护程序的复杂性
, 而C++由于需要兼容C所以更加侧重运行效率 - 程序设计其实是对于复杂性的管理: 待解决问题的复杂性, 以及用于解决该问题的工具的复杂性
- Java的功能有三种实现形式:
语言特性
,标准工具类库
,第三方类库
- Java提供的是一套表达概念的方式, 需要考虑整体的设计才能完整地运用Java的各个部分
第一章: 对象导论
面向对象编程的基本思想
- 编程的过程是构建问题空间到解空间的映射, 其中问题空间由实际问题中的各个概念以及其之间的关系构成, 解空间是被目标语言包装后的实现方式(解空间并不是直接到达物理层).
- 面向对象的设计思想是提供了一个表达问题空间元素的话语体系. 由于该编程思想中的
对象
概念和问题空间中元素
的概念有较高的相似性, 所以减轻了编程人员从问题空间映射到解空间的难度. 因此广受好评 - 对象是问题空间内的元素在解空间内的映射
- 程序员通过定义类来适应问题空间
- 任何程序都是对于目标系统的仿真, 通过面向对象可以将复杂的问题拆解为简单的解决方案. (通过拆解问题空间, 得到更易实现的子问题空间)
- 每个对象都可被视为
服务的提供者
, 它们向用户或者其他程序部分提供服务. - 可以通过
服务提供者
思维提高建模对象的内聚性. 每个对象都很好地完成其任务, 并不试图做更多事 - 每个类都被视为一个有用的代码单元, 每个类都需要被尽可能的重用, 重用的水平标志了建模的水平
Java提供的语言环境
-
Java所提供的面向对象的五个基本特性
- 万物皆为对象: 问题空间的任何概念都能够被构建成为对象
- 程序是对象的集合, 对象之间通过消息(函数调用)来推动任务的进行
- 每个对象都有自己的由其他对象构成的存储: 对象之间可以有嵌套关系
- 每个对象都有其类型: 在实现时通过
类型 -- class
标记不同种类的对象 - 某一特定类型的所有对象都可以接受相同的消息: 相同类型的对象之间互有可替代性
-
对象具有
状态
,行为
,标识
. 分别通过内部数据
,方法
,对象标识(内存地址)
进行实现 -
创建某一类型变量 == 创建对象或者实例
-
操作一个变量 == 向该变量对应的对象发送消息(发送请求)
-
某一个变量 == 某个对象的一个引用
-
类描述了具有相同特性和行为的对象集合
-
类内部的实现对于外界是不可见的
-
根据访问权限将可见性分为:
public, package(default), protected, private
-
通过对类的
实例化
,组合
,继承
,多态
实现对于类代码的重用- 实例化: 通过变量创建对象的实例
- 组合(聚合): 将现有的类以内部静态状态或者动态状态集合的方式组合为新的类.
注: 一般称动态的状态集合为聚合, 静态为组合. 其中聚合方式可以参考NS3::Node
中对于组件的聚合方式, 即使用一个容器存放所有的组件, 并在运行过程中动态地进行组件的创建, 连接, 销毁 - 继承: 继承是以现有类为基础, 复制现有类, 并对其的副本进行修改来创建新的类
- 多态: 当将一个对象作为其基类型进行对待时, 动态地调整其功能的实现细节, 以保证子类型对象状态和服务的正确性
-
继承中, 一个基类型包含了其所导出的所有类型的共享的特性和行为.
-
继承描绘的是将问题空间拆分为子空间的层次关系
-
继承支持的对于基类型功能的修改有两种: 添加, 覆盖(overriding)
-
继承就是一个替代关系
- 最完美的替代关系是完全替代, 此时基类和导出类之间是
is-a
关系, 在这种情况下可以使用覆盖进行类的导出 - 不那么完美的替代关系是
is-like-a
关系, 一般伴随着接口的增加, 在这种情况下使用覆盖和添加
- 最完美的替代关系是完全替代, 此时基类和导出类之间是
-
继承是对问题拆分层次的描述, 多态是对于拆分问题的功能实现的正确性的保障.
以下是Java中的特殊设计
-
Java单根继承的设计结构:
- 单根继承: 所有的类都有同一个最上层的基类, 所有的对象都有一个共用的接口
- 单根继承的意义: 方便为所有的对象提供基础的功能. e.g. Java中的GC, Exception处理
- 单根继承的Java实现: 所有的类都派甚自
Object
类型
-
Java中容器的设计结构:
- 容器用以解决的问题: 当不清楚需要存储的对象的具体类型, 对象存活时间, 对象存储方式的时候, 能够以较为简单的方式通过标准库组件进行使用
- Java容器的设计: 从整体上来讲, 容器就是一块可以用于存放数据的存储空间. 但是为了不同的存储方式, 设计了诸如
LinkedList, ArrayList, Set, Map
等组织方式. 通过参数化类的形式避免内存数据的类型的丢失
-
Java中对于容器使用参数化类的原因:
- 避免数据取出时发生类型的丢失
- 避免耗时且危险的向下转型. 通过编译器的展开处理为静态类型
- 可以认为是以扩大运行文件大小的方式减小危险操作的频率
-
Java对象的创建与生命周期:
- Java采用了完全动态内存的分配方式 -- 所有的对象存放于堆中
- 通过GC对对象的引用进行标记, 进行自动的内存释放
-
Java的异常处理:
- 异常处理在代码格式上是使用一个类进行抛出
- 异常处理分为两个步骤: 抛出异常, 捕获并处理异常
- 异常处理与C++中返回状态的区别:
- 异常对于正常代码逻辑入侵较小
- 返回状态可以被人为忽略但是异常必须经过处理
- 异常的捕获是与正常执行路径并行的一条路径, 独立于业务主逻辑
- 异常在Java中处理为对象的形式, 但是其并不是OOP概念, 而是类似于信号量的进程间通讯的概念
-
Java的并发编程:
- 进行并发的目的是提高应用的实时响应能力
- Java中使用线程进行并发
- 当操作系统支持多处理器时, Java的多线程并发可以变化为多核并行
- Java对于并发与并行中需要使用的锁等其他功能有库进行支持
Java的应用环境
-
编程人员可以分为两大类: 类的创建者, 应用的实现者
- 类的创建者:收集应用实现者的需求并创建新的数据类型
- 应用的实现者: 收集各种解决某些问题的类, 并根据业务进行组合为应用
-
Java虽然确实解决了许多单机应用程序的问题, 但是其最核心的功能还是在于解决了基于万维网的程序设计问题. (可以这么说, 单机的计算机不过是工具, 开发出万维网的计算机才是信息时代)
-
信息时代基本的应用系统结构: Client - Server结构.
- Client的职责是处理当前用户的数据并进行GUI的展现, 获取用户的输入并进行同步
- Server的职责是作为中央信息存储池存放需要共享的信息, 根据分发的逻辑与参数提供并接受信息
-
Web服务中, 相对于网页数据的存储与分发, 对于网页进行图形化显示的总性能消耗其实更大. 但是为什么网络服务的主要瓶颈是服务端呢? 是因为客户端的系统是分布式的, 其性能上限更高. 但是作为数据交换中心的服务端会形成全局性能瓶颈
-
客户端编程的几个方式(编程难度由易到难)
- 静态页面: 只提供静态的html页面, 不进行用户的输入
- 通用网关接口页面(CGI): 基于html的简单数据收集功能, 通过拼接request进行信息输入
- 脚本语言编程: 基于浏览器插件, 进行脚本语言的编写(e.g. JS). 不同的脚本语言需要使用不同的插件进行支持, 例如: flash, css 等
- 浏览器小程序: 基于更加跨平台的API, 在浏览器的基础上进行小程序的编写与分发. 这种小程序基本上算作运行于浏览器环境的独立程序. 例如: 基于
Java Web Start
运行的Java Applet
程序, 微软基于.NET
环境的C#
应用 - 完整的独立应用程序: 直接基于物理机系统构建应用程序, 并通过应用程序内部的网络通信功能进行信息的传输
注: 这里的分类是一个比较笼统的分类, 并不意味着使用较简单的编程方式无法编写出复杂的程序
-
几个客户端编程方式的详细解释:
-
CGI网页编程:
简单的编程范例:在浏览器端通过html的form标签进行数据的读取与发送
<form method="get" action="./cgi-bin/get"> <input type="txt" size="3" name="a">+ <input type="txt" size="3" name="b">= <input type="submit" value="sum"> </form> <!--通过拼接成为 $HTTP_PATH$/cgi-bin/get?a=XXX&b=XXX 的请求的方式传输到服务端--> 通过stdin进行字符串的输入, 进行代码解析, 使用stdout进行输出返回(类似于一个远程命令行)
int main(void){ int len; char *lenstr,poststr[20]; // 这里的poststr就是上面的a=XXX&b=XXX, 其以stdin的形式输入服务端逻辑 char m[10],n[10]; // 进行html头部的输出打印, 以stdout形式 printf("Content-Type:text/html\n\n"); printf("<HTML>\n"); printf("<HEAD>\n<TITLE >post Method</TITLE>\n</HEAD>\n"); printf("<BODY>\n"); printf("<div style=\"font-size:12px\">\n"); lenstr=getenv("CONTENT_LENGTH"); if(lenstr == NULL) printf("<DIV STYLE=\"COLOR:RED\">Error parameters should be entered!</DIV>\n"); else{ len=atoi(lenstr); // 这里在进行post request参数的读取 fgets(poststr,len+1,stdin); // 进行正则表达式的匹配 -- 通过正则表达式解析参数 if(sscanf(poststr,"m=%[^&]&n=%s",m,n)!=2){ printf("<DIV STYLE=\"COLOR:RED\">Error: Parameters are not right!</DIV>\n"); } else{ printf("<DIV STYLE=\"COLOR:GREEN; font-size:15px;font-weight:bold\">m * n = %d</DIV>\n",atoi(m)*atoi(n)); } } // 进行尾部信息的输出 printf("<HR COLOR=\"blue\" align=\"left\" width=\"100\">"); printf("<input type=\"button\" value=\"Back CGI\" onclick=\"javascript:window.location='../cgi.html'\">"); printf("</div>\n"); printf("</BODY>\n"); printf("</HTML>\n"); // 将所有的输出以stdout的形式发送出去(需要中间软件将stdout数据流转发到端口上) fflush(stdout); return 0; } 目前的CGI应用程序的服务端主要是使用
Perl
或者Python
作为request参数的解析语言
单纯CGI程序的问题在于: 响应时间慢, 无法进行图形交互, 需要重复拼接html -
脚本语言以及脚本插件编程:
脚本语言需要基于脚本插件进行运行.
例如, JS有不同的规范版本ES5, ES6, JS IE/Edge
.
浏览器根据语言规范编写浏览器插件, 对于输入的脚本语言进行解析与运行
广义的来讲, HTML可以被认为是所有浏览器的默认插件,Java Applet
,.Net
,Flex
,Flash
可以被认为是更加全能的浏览器插件
这种编程方式可以使用http协议发送自定义的消息类型(如 Json, Protobuf)并在客户端上进行解析
并且由于在本地进行一定的运算, 所以可以支持较为复杂的GUI设计以及动态响应功能 -
狭义的脚本语言编程
狭义的脚本语言主要就是指JS为代表的, 以源代码脚本的方式进行解释运行的脚本. 对于以编译后的文件进行传输以及运行的客户端程序, 一般还是认为是浏览器小程序级别的应用
-
-
服务端的编程方式:
- CGI服务端程序: 一般通过 Perl, Python, C++等语言编写单独的程序. 进行参数的解析, 计算, 拼接. CGI组件只进行地址的转换以及参数的转发
- Java Servlet服务端程序: 其实和CGI程序的思路是类似的, 只是将单独程序变化为Jar包程序. 依旧是有HttpServer作为中间转发节层. 与CGI不同的是, Servlet 的 HttpServer 提供更多易用的数据接受与发送接口, 而不是进行手动的正则数据解析与html相应拼接. 从实现角度而言, 这里的Servlet是通过对CGI进行更加细致地分层, 消耗更多的服务器计算资源以降低程序员的开发难度. 其产生的原因是互联网快速扩张时期需要快速迭代服务端功能的现实需求
Serlet后期发展出Spring 框架
, 使用配置文件进行消息处理模块的连接, 更加减轻了服务端开发复杂度. 在Spring
基础上出现Spring Boot 框架
, 其简化了Spring
的配置方式并出现微服务的概念 - SpringCloud 微服务框架: 由于互联网应用愈加复杂, 传统的以服务接口为单元的编程方式出现了代码复用率低, 维护困难的问题. 所以在
Spring Boot
基础上强化了微服务的概览, 以微服务单元作为编程目标, 更大限度地拆分任务, 提升开发效率的同时提高了模块复用频率
注: 本部分内容并没有完全基于书本内容, 参考了以下博客:
一文道尽servlet spring springboot springcloud的区别
Spring Boot和 Spring Cloud 的关系详解
微服务架构对企业来说,带来什么价值?有啥弊端?
-
服务端编程方式的演变示意图:
以下示意图仅供参考, 如需引用请标明出处
第二章: 一切都是对象
Java中的对象以及其实现
-
Java中一切都是对象
-
所有的Java对象都是存放于堆中的, 存放对象的变量其实保存的是对象的引用
-
Java也有栈存储, 其用于存放变量以及变量中所保存的引用, 以及不需要通过堆进行存放的基本数据类型
-
Java中的常量存储一般都在只读的程序代码内部
-
Java的基本数据类型
-
Java的基本数据类型并不使用GC功能也不是Object的导出类, 由于其占用空间过小, 直接存放如栈中, 通过值传递的方式进行发送
-
Java中的基本数据类型的范围以及大小并不依赖于物理机
基本类型 大小 最小值 最大值 包装器类型 自动初始值 boolean NA NA NA Boolean false char 16 bits Unicode 0 Unicode 2^16 - 1 Character 0x0000 byte 8 bits -128 +127 Byte 0 short 16 bits -2^15 +2^15 - 1 Short 0 int 32 bits -2^31 +2^31 - 1 Integer 0 long 64 bits -2^63 +2^63 - 1 Long 0 float 32 bits IEEE754标准 IEEE754标准 Float 0 double 64 bits IEEE754标准 IEEE754标准 Double 0 void NA NA NA Void NA -
Java中可以将基本数据类型直接使用等号转化为包装其类型, 也可以反向操作
-
Java中提供
BigInteger
和BigDecimal
进行高精度计算, 但是它们并不属于基本数据类型 -
对于基本数据类型, 需要保持好的习惯, 在声明时手动附上默认值.
-
Java对于基本数据类型在数组以及对象字段中会进行自动初始化, 但是对于局部变量并不会进行自动初始化
-
-
Java中对于对象数组的实现是通过引用数组的形式, 默认初始化每个应用为
null
. 也可以使用基本数据类型数组, 其中所有数据都会被初始化为其自动初始值 -
Java中的对象标识符以及命名规则:
- Java对于任何对象的标识符命名都遵循:
<包路径>.<标识符名称>
的形式. 在同一个包路径下不允许出现任何相同的标识符 - Java中使用import关键字引入其他的包, 每个包都是一个类库.
- 可以在引入路径的最后一位使用
*
进行批量引入 - Java默认的语法规则其实也有一个引入路径
java.lang
但是编译器在处理时自动引入了这个路径
- Java对于任何对象的标识符命名都遵循:
-
Java中作用域的概念仅针对存放于栈中的变量引用以及基本数据类型. 由于对象的析构是基于GC的, Java通过限制对象的访问方式(引用该对象的变量的个数)来进行作用域的限制并进行动态的GC, 所以实际对象的析构时间并不遵循作用域.
-
Java开发所作的工作就是: 定义类型, 生产对象, 操作对象
-
Java中String对象是由一串
Unicode char
组成的, 因此每个String中的字符都是16位或者说2字节(区别于C++的一个字节) -
对于Java类方法和类字段的一种解释是: 其是用于自定义类的实例化过程或者进行不限定对象的辅助过程
Java中的数值传递
- 所有的非基础类型变量都是地址引用
- 所有的函数参数都是值传递(虽然传递的是变量的引用地址)
Java推荐编程风格
-
使用java编程, 最重要的两个应用就是
javac
(进行源码的编译)和java
(进行编译后类文件的运行). 除此之外javadoc
程序也被使用进行嵌入式文档的提取 -
java的注释是通过
//xxx
或者/*xxxx*/
的形式进行书写的, 但是嵌入式文档在前面多个星号 -
嵌入式文档范例
/** 这个函数是程序的主函数 * @param args 命令行输入的字符串 * @return void 本函数没有返回值 * @throws exception 本函数没有异常抛出 */ public static void main(String[] args){ ... } -
Java要求使用大驼峰进行类以及文件的命名, 使用小驼峰对于其他任何标识符进行命名
使用javadoc进行文档的编写
基本使用流程
- 在
.java
文件中编写代码与嵌入式文档 - 使用
javadoc -d <dir-for-doc> <.java file>
对于嵌入式文档进行提取 - 在目标文件夹中形成多个相互通过超链接连接的html文件, 通过浏览器进行查看
- javadoc只会生成访问权限为
public
或者protected
的对象的文档, 因为这个文档的目的是方便他人使用你的代码库
javadoc的基本语法以及常用标签
- 所有的javadoc命令都需要写在
/** */
中. javadoc的所有标签都是以@
开头的, 并且必须置于注释行(不算开头的星号)的最前端 - javadoc会将每一行开头的
*
进行忽略, 这两个字符合在一起用于区分文档与注释 - 作者推荐的一个javadoc格式写法:
//: <path-from-project-home-to-current-file> (用于表明文件的开始) import xxx; // 进行依赖的引入 /** 对于一个类的文档 * @author 进行作者信息的标明 * 其他更多用于标注类型的标签 */ public class SampleClass { /** 对于一个字段的文档 */ public int i; /** 对于一个方法的文档 * @param ... * @return ... * @throws ... * ... 更多用于标注方法功能的标签 */ public void function(){ .... // 对于方法实现的注释 -- 不是文档 } }/* Notes: (这里是笔记部分不是文档) 对于本类型出现的问题, 需要的改进, 测试的结果进行记录 */ //:~ (这个是作者的习惯, 用于标注本源文件的结束)
-
对于一些需要手动强调的内容可以在嵌入式文档中使用html的标签, 进行字体或者内容的强调
-
一些常用的javadoc标签
标签名称 标签格式 标签作用 @see @see 在文档中生成一个名称为 See Also 的超链接用于指向你指定的类, 一般用于继承关系的文档 @link {@link # } 以 的名字创建一个指向指定类的超链接 @docRoot 产生当前文档在项目中的相对路径, 可以在文档树页面显示超链接 @inheritDoc 这个标签从当前类的直接基类的对应方法或者字段的文档中继承文档内容并先插入当前文档 @version @version 用于在版本中进行了更改的位置进行标注, 这个标签会被提取为版本更新内容, 单独作为一个页面进行显示 @author @author 用于标记类或者方法或者字段的创建者, 用于在文档中显示更改者姓名以及联系方式 @since 用于指定程序代码的最早版本, 例如jdk版本之类 @param @param 用于进行方法参数含义的标注, 其中discription可以延续数行, 直到下一个@为止 @return @return 用于标注方法返回值的意义,同样也可以写多行 @throws @throws 标明会出现什么异常对象以及每个异常对象可能产生的原因, 可有多个该标签 @deprecated @deprecated 或者写成 @Deprecated 标明某个类或者方法即将被弃用, 建议程序员更换其他实现方式 -
这些标签中, 如果两侧有大括号的, 则该标签可以作为字符串的一部分, 类似一个变量的形式进行嵌入, 如果两侧没有大括号则表面这个标签是用于每行开头, 用于表明某些意义
第三章: 操作符
-
所有对象的最底层都是通过操作符操作基础类型进行运算
-
使用某个类库的基本操作:
- 下载并解压类库文件
- 在
CLASSPATH
环境变量中添加类库文件的根目录 - 在
maven
或者其他辅助软件中引入类库
-
Java操作符中的重点是: 除了
=, == , !=
三个操作符以外, 所有其他操作符只能操作基本数据类型. 这三个操作符由于其对于引用有意义, 所以可以操作所有的对象. 另外, String类型可以通过+, +=
进行操作- 对于
String
类型的+, +=
操作, 其会尝试将后续的对象自动转化为String
类型, 所以如果是基本数据类型就可以直接进行追加, 不需要转型.
- 对于
-
对于操作符的运行优先级, 如果有迷惑的地方直接通过打括号处理即可
-
Java中能作为赋值操作的左值的对象只能是有对应内存空间的变量
-
Java中的对象的传递都是进行引用地址传递的, 所以在方法内部如果修改了某个传入参数则这个修改会影响到方法外部
-
对于需要进行对象的深度比较的时候, 通过被用户覆盖的
object.equal(anotherObject)
进行比较, 由于默认函数中自动是只支持地址比较, 所以在实现类的时候需要定义这个方法 -
Java中的布尔值是被定义只具有两种状态的抽象数据类型, 对于布尔值的大小以及实现方式为做规定, 所以其他任何基础数据类型和类的实例不能被转化为布尔值使用, 只能通过自定义的方法实现布尔值返回
-
在Java中对于基本数据类型进行初始化的数据被称为直接常量. 直接常量也有类型, 所以需要通过类型标记进行类型的规定. 例如, 指数计数法浮点数的直接常量默认是
double
类型, 所以进行float
类型赋值的时候需标明是float
类型:float tmp = 1.1e-3f; //指数计数法默认为double类型, 会出现窄化转型因此报错 -
Java中的按位操作:
& &= 按位与, | |= 按位或, ^ ^= 按位异或, ~ ~= 按位非
-
Java中定义布尔值可以进行双操作数的按位运算, 并作为单比特数据操作. 注意布尔值不能进行按位非操作, 因为这个操作的效果和逻辑not重复且容易互相混淆.
-
虽然在按位操作中
boolean
被当作了单比特数据, 但是其实际上并不是单比特数据, 因为在移位操作中不能使用布尔值进行操作 -
Java中的移位操作符为:
<< 左移操作符, >> 符号位填充右移操作符, >>> 零填充充右移操作符
. -
移位操作符如果对
char, byte, short
进行操作, 都是先将这些数据转化为int
(32位)然后进行操作, 再通过强转获得原始类型 -
移位操作符对于
long
进行操作的时候是直接操作long
-
Java中继承了C++的三元操作符:
<boolean_exp>?<value0>:<value1>
, 但是其只能放value, 更加复杂的判断操作还是通过if-else进行 -
Java中禁止对于自定义类型进行操作符重载
-
Java中对于类型的强行转换格式:
<target_type> var = (<target_type>)srcVar
-
Java中从浮点到整形都是进行截尾处理, 如果想要进行四舍五入则需要使用
java.lang.Math.round()
-
Java中没有
sizeof
操作符, 因为所有的内存使用都是自动处理的 -
Java中对于int的计算溢出不会进行警告或者报错
第四章: 控制执行流程
-
Java中唯一可以使用逗号作为操作符的地方是在for循环中:
逗号操作符的运行顺序是从左到右依次运行
for(int i = 1, j = i + 1; i < 5; i++, j = i * 2){ //... } -
Java
ForEach
语法: 适用于数组或者Iterable对象
的只读遍历for(float x : f){ //... } -
for(;;)
和while(true)
是完全一致的 -
Java中的类
goto
操作:-
通过定义label并在
continue
或者break
之后使用达到类似goto的效果outerIteration: for(int i = 0; i<10; i++){ innerIteration: for(int j = 0; j<10; j++){ continue innerIteration; // 相当于直接使用continue break innerIteration; // 相当于直接break continue outerIteration; // break掉内部循环并continue外部循环 break outerIteration; // break掉内部循环并break掉外部循环 } } -
label的定义必须先于label的使用, 而且只能用于标记循环.
-
在Java中使用标签的唯一理由就是有多层循环需要进行跳出
-
-
Java中的
switch
选择语句只能使用整形作为选择因子. 所以要使用switch必须将判断依据转化为整形, 或者使用枚举类型. 不能使用字符串或者是浮点数
第五章: 初始化和清理
Java中类的初始化
-
对象的初始化: 在对象可操作之前通过调用构造器进行初始化
class TmpClass{ TmpClass(/*xxx*/){ // 构造器这么写, 名称与类型名称相同, 没有返回值 } } -
方法重载: 在同一个类中形参不同但名字相同的功能是重载.
为什么不将返回值类型纳入重载考察范围, 因为返回值可以手动进行忽略(这种忽略返回值的功能调用方式被称为
为了副作用而调用
), 所以返回值不一定有效. 所以不能作为判断重载的依据 -
使用基础类型作为参数的重载: 如果有不需要通过类型自动转化就能运行的重载功能, 则运行该功能, 反之使用需要最少转换的重载 注意, 这里的使用并不清晰, 所以需要避免进行有多个功能都是最小转换的重载定义
-
Java中的默认构造器就是直接将对象对应的内存空间完全置零(而数据中的0会被解释为基础类型的自动初始值以及引用类型的
null
) -
为什么当自己定义了一个构造器之后, 默认构造器就会失效: 因为如果不失效则提供了一种破坏对象创建逻辑的创建方式, 影响创建对象的完整性与安全性.
-
本书中解释
static
方法是没有this的方法
. 但是static方法在面向对象中也有其作用, 其一个核心的作用就是控制对象的生成方式. 但是在Java中不应该滥用静态方法. 如果发现自己代码中过多的出现静态方法则需要考虑是否是自己的程序设计出现了问题
Java中对象的清理: 终结处理与垃圾回收
-
Java中提供了一个接口(
finalize()
)用于进行终结处理. 其正常使用的环境局限为当在Java中通过本地方法机制
调用了C或者C++对象的情况. 由于本地方法中创建的对象没法使用JavaGC进行垃圾回收, 所以需要手动设置一个finalize()
函数, 在进行GC的时候首先进行自定义的终结处理步骤 -
终结处理接口在对象被触发GC的时候发生调用. 其还有一个特殊的用途, 用于在对象回收的时候验证对象是否处于可回收状态(用于bug的发现).
// 例如某个书籍对象必须要是被还回来后才可以被删除, 在finalize中验证其是否状态为"已归还" class Book{ private boolean isReturned = false; // 插入一些用于操作isReturned字段的方法 protected void finalize(){ //super.finalize(); // 总是假设基类的终结处理中进行了某些重要工作, 但是这里需要进行额外的异常处理, 所以暂时注释掉 if(!isReturned){ System.out.println("Error: 书籍未被归还记录就已经被删除"); // 或者使用抛出异常也是可以的 } } } public class MainClass{ public static void main(String[] args){ Book novel = new Book(); //... 某些业务操作 novel = new Book(); // 覆盖原本的对象, 使得其可以被回收 System.gc(); // 强制系统进行一次回收操作 } }/* output: Error: 书籍未被归还记录就已经被删除 */ -
关于
finalize()
的争议: 由于其是在触发GC的时候自动运行, 所以当内存足够大的时候, 可能永远也无法触发GC, 因此在不进行强制GC的时候这个函数的调用是不确定的 -
注意: Java中是没有析构函数的, 内存的使用会被GC系统自动回收, 但是某些情况下会需要在对象析构前进行一些重要操作, 例如: 文件解锁, 网络链接关闭等. 这些功能在Java中需要以普通功能的形式在程序逻辑中进行手动的处理, 不能依赖于GC. 例如: 文件的
close
函数就是这一类 -
Java中的GC机制:
- Java中GC功能的主要效果有两个: 找到不再使用的对象并进行删除 , 对于正在使用的对象进行重新组织以获得连续的内存空间
- 一种简单但是没有人在Java中使用的方法 – 引用计数法:
- 基础逻辑: 在每个对象上都有一个计数器, 当发生引用的时候就加一, 如果引用被覆盖或者删除则减一. 当一个对象的计数器到达0之后就进行回收
- 设计问题: 当出现对象之间循环引用的情况时, 被循环引用的整个环的计数器都无法到达0, 也就无法被析构, 造成了实质性的内存泄露. 虽然可以通过其他技术判断是否成环, 但是由于进行判断成环的图遍历效率过低, 所以不能进行使用
- Java实际使用的GC的基本思想:
- 有意义的对象是
活动
的对象, 活动的对象意味着从目前的变量中可以通过某种方式访问到它. - 要找出所有的
活动对象
, 则进行一个以栈中变量为开始节点的广度优先的图遍历
. - 对于每个被找到的活动对象则在上面打上记号, 基于所有最新打上记号的对象, 寻找其引用的没有打上记号的下一批对象
- 由于是通过打记号的方式进行遍历, 不涉及路径的判断, 所以对于循环引用可以进行很好的处理
- 有意义的对象是
- Java的自适应垃圾回收技术:
- 上面的基本思想只实现了找到未使用的对象并删除的功能, 没有完成使得空余内存合并为连续内存的任务
- 自适应垃圾回收技术是一种根据不同情况使用不同的内存整理方式的技术, 使得在获得较好内存整理效果的同时不会占用过多的运行资源
- 自适应垃圾回收的第一种策略是: 停止–复制策略:
- 当有大量垃圾积累或者是内存碎片过多的时候, 使用本策略.
- 先暂停正在运行的进程, 标记所有的活动对象, 在内存中新建一个空白堆, 将所有的活动对象一个接着一个复制到空白堆中, 对虚拟机中的所有引用变量进行重新映射, 删除原先堆中的所有数据.
- 这种垃圾回收机制是前台的垃圾回收, 非常消耗资源. 为了提高这种垃圾回收的效率, 将整个程序的堆分为几个小堆, 对每个小堆进行单独的垃圾回收, 提高内存利用率和并行度
- 自适应垃圾回收的第二种策略: 标记–删除策略:
- 当程序进入稳定运行状态的时候, 可能只会有较少的内存垃圾出现, 甚至没有内存垃圾, 这个时候使用需要复制所有活动对象的停止复制就非常不划算
- 虽然在清理相同大小的内存空间时,标记–删除策略相比停止–复制策略更慢, 但是由于其不进行活动对象的复制, 所以当垃圾很少的时候其相比停止–复制而言效率更高.
- 其算法同样是以广度优先遍历的方式对所有的活动对象进行标记, 当标记完成后, 对于所有不活动的对象进行删除. 虽然会暂停程序并产生内存碎片, 但是由于垃圾总量很少, 所以对于程序运行影响不大
- Java自适应垃圾回收策略的转换:
- 当出现较大的内存不足或者是较多的内存碎片的时候, 通过
暂停-复制
策略进行垃圾回收 - 当长时间没有进行垃圾回收或者垃圾总量较少的时候, 使用
标记-删除
策略进行垃圾回收
- 当出现较大的内存不足或者是较多的内存碎片的时候, 通过
- Java其他提高运行效率的机制:
- Java
Just in time
编译技术: 通过将本地程序全部或者部分转化为机器码减少代码的大小, 提高运行效率 (因为类解释执行会需要很多不必要的代码) - Java 惰性评估机制: 编译器只在需要的时候才进行源码的编译, 从来不被执行的代码不会被编译为机器码. 新版Java的
HotSpot
技术也是基于此进行修改的: 代码每次执行的时候, 根据被执行到的内容进行动态的编译和替换, 代码运行次数越多, 编译器就越清楚哪部分代码更重要, 也就能更优化对应代码 – 代码执行的次数越多, 代码优化程度越高, 代码运行速度越快
- Java
Java中字段的初始化
-
字段可以在定义的时候附上默认值, 这被称作
指定初始化
-
在进行字段
指定初始化
的时候, 可以调用本类型的函数进行初始化(包括static和非static). 但是由于初始化的顺序为排除功能定义外的定义顺序, 所以需要注意所使用的初始化方式是否依赖了未被初始化的对象class Trial1{ private int a = 1; private int b = a+1; private int c = func(); private int d = staticfunc(this); private int func(){ return this.a + this.b; } private static int staticfunc(Trial1 self){ return self.a + self.b; } } -
对于静态对象的默认值遵循基础类型以及引用类型的自动初始值, 也就是如果是对象, 其初始值为null
-
需要注意的是: 静态字段的初始化遵循必要原则, 如果某个类没有被任何的实例化并且没有调用该静态字段的代码, 则这个字段不会进行初始化. 只在第一次实例化对象或者在第一次调用静态字段的时候进行初始化
-
在一个对象内部的初始化顺序为: 静态对象 -> 非静态对象
-
详细的实例化顺序为:
- Java解释器寻找类路径
- 载入
.class
文件并执行静态对象初始化 - 对于非静态对象分配只够空间
- 对空间进行清零
- 执行处于字段定义中的指定初始化
- 执行构造器
- 返回被实例化的对象引用
-
静态子句初始化:
public class Trial{ static int i; static { // 这个是静态子句,用于进行静态对象的复杂初始化. 相对于指定初始化的复杂版 i = 100; System.out.println(i); } } -
非静态变量子句初始化:
public class Trial{ int i; DefinedClass d; { // 和非静态变量的指定初始化一致, 在构造函数之前进行运行 i = 100; if(i == 100){ d = new DefinedClass("sample input"); }else{ d = new DefinedClass(); } } }
Java中数组对象的初始化:
-
数组虽然看起来像是个基础数据类型, 但是其实际是基于Object的对象
// 虽然 int a[] 也有效, 但是 int[] a 更符合实际 int[] a = {1,2,3,4,5}; // 等效于以下代码: int[] a = new int[5]; a[0] = 1; a[1] = 2; a[2] = 3; a[3] = 4; a[4] = 5; //或者这么写: int[] c = new int[]{1,2,3,4,5}; // 注意: 如果使用了大括号进行赋值, 则不能在方括号中填写长度 // 如果使用对象的数组可以进行Autoboxing Integer[] b = new Integer[]{ new Integer(1), new Integer(2), new Integer(3), 4, // 如果不进行boxing会自动转换为Integer类型 new Integer(5), // 最后一个逗号会自动忽略 }; -
基于数组对象实现的可变参数列表:
public static void printArray1(Object[] args){ for(Object o : args){ System.out.print(o+" "); } System.out.println(); } public static void printArray2(Object... args){ for(Object o : args){ System.out.print(o+" "); } System.out.println(); } // printArray1 和 printArray2 在效果上是完全等效的, 只是调用和定义的语法不同 public static void main(String[] args){ printArray1((Object[])new Integer[]{1,2,3,4}); // printArray1(new Integer(1),new Integer(2),new Integer(3),new Integer(4)); // 这种写法不正确 //printArray1(); // 这种写法不正确 printArray2((Object[])new Integer[]{1,2,3,4}); printArray2(new Integer(1),new Integer(2),new Integer(3),new Integer(4)); printArray2(); } -
由于可变参数导致的模糊性问题:
static void f(float f, Character... args){...} static void f(Character... args){...} public static void main(String[] args){ f(1,'a'); // 因为有不需要转换的函数, 所以没有报错 f('a','b'); // ERROR: JavaMainTest 中的方法 f(float,java.lang.Character...) 和 JavaMainTest 中的方法 f(java.lang.Character...) 都匹配 // 因为 (char)'a' -> (int)97 -> (float)97.0 有自动转换功能 -- 来自Autoboxing } // 应该改写为: 通过非可变参数确定功能调用, 避免autoboxing static void f(float f, Character... args){...} static void f(Character c, Character... args){...} -
所以在Java中使用可变参数需要非常谨慎, 避免与其他功能产生模糊
枚举类型与switch
-
简单示例:
//: Spiciness.java public enum Spiciness{ NOT, MILD, MEDIUM, HOT, FLAMING } //:~ //: Main.java //... public static void main(String[] args){ Spiciness tmp = Spiciness.HOT; System.out.println(tmp); // output: HOT //会直接打印出常量名称 for(Spiciness s: Spiciness.values()){ // 遍历所有常量 System.out.println(s + ", ordinal" + s.ordinal()); // 打印出常量名称以及对应的整形数值 // 这里的整形数值是从0开始的递增整数 } // 将枚举类型用于switch条件 switch(tmp){ case NOT: //... break; case MILD: case MEDIUM: //... break; case HOT: case FLAMING: default: // ... } } //:~
第六章: 访问权限控制
- 访问权限控制的主要目的是隐藏具体的实现.
- 代码编写很难做到一次性实现最佳的写法. 在编程过程中对于代码的重构是不可避免的. 而重构代码的目的是为了
更高效, 更可读, 更具有维护性
- 访问权限控制就是为了增加代码复用, 减少重构压力的结构化工具
- 四种访问权限:
public
,private
,protected
,package(default)
- java中包的意义和C++中的命名空间类似, 是用于指引编译器找到对应类的地址
- 一个.java文件是编译的一个单元, 每个编译单元中必须有一个和文件名一致的公开类, 其他所有类都必须不能为公开类
- 对于.java文件编译的结果是多个.class文件, 对于一个编译单元中的每一个类型都会输出一个.class文件
- Java的运行文档是将多个.class文件打包压缩为一个.jar文件. 使用Java虚拟机对于该文件中的类型以及数据进行查找, 转载以及解释
- 一个类库一般是一个包的形式进行组织, 该包中有一组类文件, 每个文件有一个public类, 以及任意数量的非public类型
- 在Java中限制所有的包名为小写, 类名为大驼峰
- Java编译器寻找类库的方式是通过
CLASSPATH
环境变量, 以这些环境变量为根目录, 以每一层包的名称作为文件夹名称进行文件树的查询. 一般类库是以.jar格式进行存储的 - 代码库的导入: 使用
import
和import static
- 使用import改变代码的行为: 可以通过对于debug版本的类库设置统一格式的引入路径, 使得对于不同状态的代码只需要修改引入名称即可进行调整
- 编写代码中的一个重要内容就是尽可能减少实现的暴露
第七章: 复用类
- Java中对于类的复用是通过类的组合以及类的继承进行实现的
- 类的组合是通过将原始类作为新类的字段进行实现
- 类的继承是通过继承语法对于基类的接口以及实现进行继承以及修改
- 继承语法:
public class A extends B { public A(){ super(); // 后续处理步骤需要在初始化基类之后 } @Override // 该标记是让编译器帮忙确定下面的函数时覆盖而非重载, 如果出错编译器会报错 public void trial(){ super.trial(); } }
-
代理语法的类的复用(这更多的是一种封装方式), 通过创建功能用来调用私有字段的同名功能, 使得类型内部的结构不会暴露到使用者面前
-
对于对象的析构: 慎用
finalize()
接口. 一般所有内存的析构都是交给GC, 如果是需要保证在程序完成前进行一些操作(例如文件的解说, 端口的占用解除等)不能使用finalize()
而需要手动定义clean
函数并在需要清理的时候手动调用 -
Java类中对于功能的重载即使在多重继承的情况下也可以使用. (该功能在继承深度较深的情况下容易导致接口的不清晰)
-
组合和继承的技术选择:
- 组合是显式的对于代码的重用, 一般使用于需要使用某个类型的功能但是不使用该类型的接口的情况下
- 继承是通过隐式的方法将基类的代码导入子类中, 其一般使用于需要使用某个类型的一个特殊版本的情况下
-
protected
对于继承的意义: 其不可被用户访问但是可以被子类的程序员访问, 一般是类中的某些重要组件, 可能在子类的实现中需要使用的内容 -
继承的更加主要的应用方面是对于对象的向上转型, 使得某一模块可以同时和多个类进行适配, 增加了编程的灵活性
-
类的继承应当被慎用, 更加频繁地使用组合而不是继承对于代码地可读性和结构化有帮助
-
final关键字: 表示后面的数据, 方法或者类是不能被改变的
-
final 对于基本类型的变量, 是限制其内部值不能发生改变
-
final 对于一个类型的引用, 是限制其引用的对象不能发生改变, 但是其内部内容是可以发生改变的
-
static final 可以被用于定义全局常量, 其会被编译为一个全局无法改变的空间, 一般使用全大写字母以及下划线进行命名
-
blank final 可以在类中定义没有被初始化的final对象, 这些对象需要在构造函数中进行赋值. 可以使用变量进行赋值
public class A { private final int i; public A(){ i = 1; } public A(int x){ i = x; } } -
final 形参, 可以将函数的传入参数设置为final. 这样的话在函数内部就不能更改该形参所引用的对象
-
final 方法: 使用final修饰的方法不可被继承的子类所覆盖. (注意会发生覆盖的功能都是public的功能, 是对象的接口)
-
final 类: 该类型不能出现任何变动, 也就是不允许继承该类生成子类
-
-
static 对象的初始化为需要使用该对象的第一次之前
-
继承的初始化顺序是先基类后子类
-
在设计一个系统时, 目标应该是找到或者创建某些类, 其中每个类都有具体用途, 而且既不会太大(会包含太多功能难以复用), 也不会太小(不添加其他功能会导致不能使用 – 耦合性过高). 如果设计过为复杂可以通过将现有类型拆分为更小的部分进行修复
-
程序的开发是一个增量的过程, 程序的开发依赖于实验, 虽然可以在开始编写前尽己所能进行分析, 但是仍然不能完全认识到所有的答案. 可以将编程的项目视作一个有机的进化着的生命体, 而不是打算像盖摩天大楼一样快速见效, 就会获得更多的成功和更迅速的回馈
-
继承与组合是面向对象程序设计中进行程序增量开发的基本工具
第八章: 多态
- 多态是对于向上转型的另一种理解. 通过将接口和实现进行分离, 更改代码的结构和可读性. 使得程序的模块具有可增长的能力
- Java对于所有的多态都是动态绑定, 无法使用基类的功能
- 静态方法与私有字段不具有任何的多态功能
- 构造器是不能够进行多态的, 因为构造器隐式地声明为
static
- 多态函数的返回值最好不要进行改变, 虽然可以在基类到子类之间进行改变, 但是如果改为其他类型则无法进行覆盖
- 关于多态的使用: 使用继承表达行为之间的差异, 使用字段表达状态上的变化
- 继承的两种分类
- 纯继承: 子类的接口和基类的接口完全一致. 是子类对于基类的一种完全替代. 需要表达的是一种
is a
关系 - 拓展继承: 子类在继承了基类的接口的同时还定义了其他的功能, 表达的是一个
is like a
关系. 其最容易导致的问题是通过拓展继承实现的子类在进行向上转型的时候无法访问新增的方法
- 纯继承: 子类的接口和基类的接口完全一致. 是子类对于基类的一种完全替代. 需要表达的是一种
- 向下转型和运行时类型识别
- 向下转型是手动的, 且危险的. 所以Java自动在每次转型的时候都进行检查 运行时类型识别
RTTI
- 语法:
(ChildClass) baseClassObject
- 如果转型不成功, 会返回
ClassCastException
- 向下转型是手动的, 且危险的. 所以Java自动在每次转型的时候都进行检查 运行时类型识别
第九章: 接口
- 是将接口和实现分离的方法, 一般分为纯接口或者抽象类
- 语法:
// 抽象类 class A { abstract public void funct(); // 定义了一个抽象方法(相当于C++中的纯虚函数) // 含有抽象方法的类都是抽象类 // 对于继承于抽象类的子类, 其抽象方法必须提供定义 // 不能够为抽象类创建对象 }
- 抽象类的意义在于, 其能够明确地表示一个类型的使用方法, 避免构建错误的实例, 以及避免忘记定义需要覆盖的函数
- 接口: 是对于完全抽象的类的另一种称呼. 只允许提供功能的名称, 参数, 返回值. 并且这些方法隐式地被声明为
public
. 常用于作为类之间的连接部件. - 接口内部也可以定义字段, 但是这些字段被隐式地声明为
static final
的 - 完全解耦的面向接口的编程;
- 在接口中定义功能以及功能的描述, 在实现了接口的类中直接编写符合某一语境下的功能, 即可完成编程
interface intf{ void funct(); } public class A implements intf { void funct(){ // ... } }
-
对于接口的多重继承:
- 由于对于类的多重继承会导致类的实现之间的冲突, 所以一般在需要某个类实现多个功能的情况下, 使用对于接口的多重实现
- 子类需要实现所有接口中功能的并集. 如果出现接口定义的功能之间的冲突, 会在编译的时候报错
-
接口之间的继承:
- 为了可以在原始版本上扩展接口的功能, 接口之间可以使用
extends
关键字对于接口进行继承
- 为了可以在原始版本上扩展接口的功能, 接口之间可以使用
-
使用接口中的字段进行常量的定义
- 因为接口中的字段自动声明为
static final
所以它们可以用于声明全局常量 (特别是在枚举类型enum
出现之前)
public interface Months{ int JANUARY = 1, FEBRUARY = 2, MARCH = 3 ;//... } public class A{ public static void main(String[] args){ Months.JANUARY; } } - 由于接口不存在初始化函数, 所以所有的常量在被定义之后必须立即赋值. 否则出现的
空final
无法再进行赋值
- 因为接口中的字段自动声明为
-
嵌套接口: 主要是为了语言的一致性进行考虑, 实际中没有什么用处
-
基于接口的工厂设计模式:
- 多个具体类型, 和一个类型接口, 加一个工厂类
- 工厂类可以通过输入动态地选择实例化什么类型, 并使用类型接口输出
第十章: 内部类
- 形式上是将一个类定义在一个类的内部
- 内部类不仅是一个代码隐藏机制, 而且可以与外部类字段进行直接的通信
public class A{ class B{ int fieldB; public void functB(){ System.out.println(fieldA); // 通过闭包的形式, 内部类可以直接访问外部类的字段 } } int fieldA; public B functA(){ return new B(); } static public void main(String[] args){ A a = new A(); A.B b = a.functA(); // 内部类的名称需要使用外层类加上点 } }
-
非静态内部类一般只在需要与外部类有关联的时候进行创建. 静态内部类相当于直接定义外部类
-
在进行内部类的实例化的时候需要指定一个外部类对象, 所以一般非静态内部类的实例化是通过在外部类中定义一个非静态函数来调用
new
语句 或者使用 带有对象的new语句public class A{ class B{} public B getB(){return new B();} // 通过在外部类中创建获取函数 public static void main(String[] args){ A a = new A(); A.B tmp = a.new B(); // 通过带对象的new语句进行实例的获取 } } -
在内部类中获取外部类绑定的实例:
public class A{ class B{ public A getOutterA(){ return A.this; } } public static void main(String[] args){ A a = new A(); B b = a.new B(); System.out.println(a); System.out.println(b.getOutterA()); // 两者返回的对象是一样的 } } -
内部类的一个重要的使用场景: 为一个类提供符合一个接口的对象, 并隐藏所有的实现. 其的价值在于可以将对象的数据和对象的接口进行分离. 将对象对于接口的匹配可以不用再更改对象结构
interface StrInterface{ String getStr(); } interface IntInterface{ int getInt(); } public class InternalClassTrial{ public static void main(String[] args) { OutterClass trial = new OutterClass(); StrInterface strInterface = trial.getStrInterface(); // 获取该对象的指定接口 StrInterface IntInterface intInterface = trial.getIntInterface(); // 获取该对象的指定接口 IntInterface System.out.println(strInterface.getStr()); System.out.println(intInterface.getInt()); } } class OutterClass{ private String str = "100"; private int i = 100; private class InnerClassStrInterface implements StrInterface{ @Override public String getStr() { return str; } } private class InnerClassIntInterface implements IntInterface{ @Override public int getInt() { return i; } } StrInterface getStrInterface(){return new InnerClassStrInterface();} IntInterface getIntInterface(){return new InnerClassIntInterface();} } -
在方法和作用域中的内部类
- 之前直接在类的内部进行内部类的创建的写法是最简单的内部类实现方式
- 写在方法和作用域中的内部类一般是为了临时解决一些接口适配问题, 这些内部类对于外部都是不可见的
class OutterClass{ private String str = "100"; private int i = 100; StrInterface getStrInterface(){ class InnerClassStrInterface implements StrInterface{ // 将类型的定义放在方法内部, 和前面的定义方式是一致的 @Override public String getStr() { return str; } } return new InnerClassStrInterface(); } IntInterface getIntInterface(){ class InnerClassIntInterface implements IntInterface{ // 由于在方法内部, 无法声明访问权限,所以全是private. 其有效范围是作用域范围 @Override public int getInt() { return i; } } return new InnerClassIntInterface(); } } - 对于可以一些情况下, 内部类型只使用一次, 可以使用匿名内部类的形式进行定义以及实例化 (匿名内部类一般只使用在作用域内)
class OutterClass{ private String str = "100"; private int i = 100; StrInterface getStrInterface(){ return new StrInterface() { // 将作用域中的内部类改为临时匿名类, 作用效果完全一致 @Override public String getStr() { return str; } }; } IntInterface getIntInterface(){ return new IntInterface() { @Override public int getInt() { return i; } }; } } -
基于匿名内部类实现工厂设计模式
public class InternalClassTrial{ public static void main(String[] args) { FactoryInterface factory = HelloWorldObject.factory; // 直接获取静态工厂对象 ObjectInterface obj = factory.getObject(); obj.printInfo(); } } interface ObjectInterface{ void printInfo(); } interface FactoryInterface{ ObjectInterface getObject(); } class HelloWorldObject implements ObjectInterface{ private String info = "HELLO World!"; private HelloWorldObject(){} // 禁用在外部直接创建对象的构造函数 // 以匿名类的形式定义一个静态的工厂类型并将其绑定于对象之上 public static FactoryInterface factory = new FactoryInterface() { @Override public ObjectInterface getObject() { return new HelloWorldObject(); } }; @Override public void printInfo() { System.out.println(info); } } -
静态内部类(嵌套类) — 只是定义在其他类内部, 对外部类并没有联系
- 普通的内部类以及域内部类中不能存在静态方法 — 因为其的创建依赖外部类的数据, 没有外部数据就没有意义. 所以不能静态
- 静态内部类可以有静态方法和字段
-
接口内部类 – 接口中的内部类都是自动
public static
的, 所以其只是相当于命名空间一样, 在名字中嵌套了接口名称 -
内部类的一个使用场景: 在一个类内部定义其的测试代码
-
多层嵌套类可以直接访问外部类, 因为闭包是忽略嵌套层数的
-
为什么需要内部类:
- 内部类一般用于实现一个类型的某个接口, 内部类提供了某种进入外部类的接口
- 一个外部类的对象的内部类对象可以有多个, 每个对象实例都有自己的状态信息, 与外部类进行独立
- 单个外部类可以通过定义多个内部类来对一个接口进行多种实现
- 创建内部对象的时间不由外部对象创建时间决定
- C++中类似内部类的编程语法是友元, 但是友元会影响代码的结构性
-
闭包和回调
- 闭包的定义: 闭包的对象是可调用对象, 它记录了一些创建其的作用域内的信息
- 内部类可以视作是对于外部类的一个闭包
- C++中的回调结构在Java中是通过闭包进行实现的, 通过闭包生成一个满足特定接口的内部类进行功能的调用
- 由于内部类是Java中对于回调的主要实现方式, 所以内部类经常被用于一些程序框架内部. 通过定义事件以及与该事件绑定的内部类进行事件的响应
-
内部类的继承
- 根据语法完备性, 构造的语法.
- 注意, 内部类的继承只能继承内部类本身, 无法访问外部类的的元素. 而且由于内部类初始化时需要提供外部类引用, 所以需要手动通过继承类引用进行实例化
class OutterClass { class InnerClass{} } class InheritInner extends OutterClass.InnerClass { InheritInner(OutterClass outterObj){ outterObj.super(); // 需要外部实例进行实例化 } } class Main{ public static void main(String[] args){ OutterClass outterObj = new OutterClass(); InheritInner inheritObj = new InheritInner(outterObj); } } -
使用内部类而不是匿名类的唯一原因就是需要创建多个内部类对象
-
内部类的编译以及命名. 外围类的命名会使用
$
进行分割. 如果是匿名的就会随机生成一个名称
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】