普通视图

发现新文章,点击刷新页面。
昨天以前Java for You

还在手写 Getter/Setter 方法吗?Lombok 让你的代码更简洁!

作者 蜗牛
2022年6月3日 14:42

以下是文章大纲:

lombok

Lombok 是什么?

官网:https://projectlombok.org/

Lombok 是一个 Java 库,使用 Lombok 可以通过简单的注解帮助我们消除 Java 的样板代码,使代码更加简洁清晰。

比如对于简单的 Java 对象(POJO),很多的代码里充斥着大量的 getter()setter() 方法,样板代码占用比例高,影响可读性,引入 Lombok 只需一个注解就能达到相同效果,而且更简洁。

Lombok 引入

1、POM 中引入依赖

使用 Lombok 注解需要依赖它的库。

Maven 库:https://mvnrepository.com/artifact/org.projectlombok/lombok

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.16</version>
</dependency>

系统中如果有引入无需重复引入。

2、IDE 中安装插件

为什么装插件?

  • 因为 Lombok 的引入后,java 文件使用 javac 编译成的字节码文件中就会包含 getter 和 setter 函数,但是源代码中找不到定义,IDE 会认为这是错误,因此需要安装一个 Lombok 的插件,方便本地 IDE 的研发工作。

IDEA 中安装:

Settings->Plugins->输入框输入”lombok”

图片

Lombok 使用

比如要针对以下 Java 对象提供 getter()setter() 方法:

public class Mountain {

    private String name;

    private String country;
}

只需要在类上打 @Getter@Setter 注解。

@Getter
@Setter
public class Mountain {

    private String name;

    private String country;
}

然后用 maven 编译一下:mvn clean compile

使用的时候就可以正常拿到像 getCountry()setCountry() 这样的方法。通过 IDE 找方法调用处也可以直接锁定到字段上,以前要点两下才能看到字段明细信息,使用 Lombok 点一下就能达到效果。

图片

Lombok 使用注意

  • 不建议直接使用 @Data 这种大而全的方式覆盖太多的简化场景。
  • toString 场景不建议使用 Lombok 的,建议使用系统自定义 ToString 里的方法。
  • 对外包考虑到接入方也需要引入依赖,如果担心副作用,可以不引入。
  • 其他注解酌情使用。

Lombok 原理

自 JDK6 之后,javac 在执行的时候会调用实现了 JSR 269 API 的程序,这样我们就可以对编译器做一些增强,这时 javac 执行的过程如下:

图片

Lombok 本质上就是一个实现了“JSR 269 API”的程序。在使用 javac 的过程中,它产生作用的具体流程如下:

  1. javac 对源代码进行分析,生成一棵抽象语法树(AST)
  2. 运行过程中调用实现了“JSR 269 API”的 Lombok 程序
  3. Lombok 对第一步骤得到的 AST 进行处理,找到 @Getter @Setter 注解所在类对应的语法树(AST)
  4. Lombok 修改该语法树(AST),增加 getter()setter() 方法定义的相应树节点
  5. javac 使用修改后的抽象语法树(AST)生成字节码文件,即给 class 增加新的节点(代码块)

好了,今天的分享就到这里,我很多项目在工程实践中都引入了 Lombok,对编码效率提升很大,读者朋友可以尝试使用,也可以和我交流心得。如果这篇文章对你有用的话,欢迎分享转发,这会对我有很大的帮助。


推荐阅读:

Java 面向对象一览

Java 核心类库一览

更多内容请收藏:Java for You

【Java面试题01】基础知识精选

作者 蜗牛
2022年6月3日 14:38

Java面试

前言

如果你是 Java 后端方向,Java 基础的知识就必须要相当熟悉。

蜗牛也做过面试官,本篇文章就从面试官的角度上,精选出 Java 基础相关的题目,我会给出参考答案,也会指明考察点,欢迎大家一起互动交流。

Java面试题目

1、什么是 JDK?你常用的版本是哪个?为什么用这个版本?

【考察点】

对 Java 开发工具的理解,以及技术选型方面的思考

【参考答案】

JDK 全称 Java Development ToolKit,直译一下就是 Java 语言开发工具包。

JDK 包含了 开发者工具比如 javac 用来编译 java 源码,jar 用来打包,还有一系列其他工具。此外,还包含了 JRE(Java Runtime Environment),也就是 Java 应用程序的运行环境,它除了运行程序的 java 指令外,还有类库以及执行 Java 应用程序的 JVM(Java 虚拟机)。

我常用的版本是 JDK8。一方面是这个版本已经比较稳定,另一方面,这个版本使用非常普遍,出了问题,很容易在网上找到答案。

图片
 

2、一个Java程序从编码到运行,这中间发生了什么?

【考察点】

对 Java 程序运行原理的理解

【参考答案】

  1. 首先编码之后得到 .java 后缀的源代码文件。
  2. 用 JDK 中的 javac 命令将 Java 源代码进行编译,生成 Java 字节码,也就是 class 文件。
  3. 用 JRE 的 java 命令执行 class 文件时,Java 字节码会被传输到 JVM(Java 虚拟机),JVM 会合并字节码以及 JRE 中的库文件一起执行,输出特定硬件平台的机器码,或者说指令集。
  4. 机器码被底层物理硬件平台执行
Java面试
 

3、一个极简可运行的 Java 程序,它的代码有哪些要素?

【考察点】

对 Java 编码规范的理解

【参考答案】

  • 类类型
  • 类名
  • 访问修饰符
  • 成对的花括号
  • main 方法
    • 执行语句
图片
  

4、Java 基本类型里的布尔类型占用多大的内存?

【考察点】

对 Java 基本数据类型的理解

【参考答案】

1 字节或 4 字节。

布尔类型表达是或者否,只有 trueflase 两个值,用关键字 boolean 表示,但 JVM 没有针对 boolean 的字节码指令,因此在虚拟机规范里,boolean 类型在编译后会被 int 代替,占用 4 个字节,如果是 boolean 数组,会被编译成 byte 数组类型,每个 boolean 数组元素占 1 个字节。实际情况就取决于各厂商发布的 JVM 实现了。

5、& 和 && 有什么区别?

【考察点】

对 Java 基本类型运算的理解

【参考答案】

&&:逻辑与运算符。当运算符左右两边的表达式都为 true,才返回 true。同时具有短路性,如果第一个表达式为 false,则直接返回 false。

&:既能当做逻辑与运算符,也能当做按位与运算符。

逻辑与运算符:& 在用于逻辑与时,和 && 的区别是不具有短路性。所以通常使用逻辑与运算符都会使用 &&,而 & 更多的适用于位运算。

按位与运算符:用于二进制的计算,只有对应的两个二进位均为1时,结果位才为1 ,否则为0。

6、以下代码为什么输出的不是 129?

int highIntValue = 129;
byte lowByteValue = (byte)highIntValue;

System.out.println(lowByteValue);

【考察点】

考察对计算机原码、反码和补码的理解

【参考答案】

这里会输出 -127,而不是 129。

在上面代码中,我们知道,int 类型数据是 32位,byte 类型数据为 8 位,Java 把 int 类型数据转成 byte 类型数据时,实质上是截取 int 后 8 位存到 byte 中。

int 类型的 129 三码(原码、反码和补码)一致,都为:0000 0000 0000 0000 0000 0000 1000 0001。计算机中存的是补码

从 int 转换 byte,截取后 8 位为:1000 0001。得到的数据为依然是补码

负数补码转原码的公式:

  • 负数原码=(补码-1)&&数值位取反

我们按照公式,会发现其原码为:补码(1000 0001)–> 反码(1000 0000)–> 原码(1111 1111)。即 1111 1111就是 (byte)highIntValue 的结果。

转换成十进制就是 lowByteValue=-(64+32+16+8+4+2+1)=-127。

7、new String(“xxx”); 这行代码会产生几个对象?

【考察点】

考察对 Java 字符串存储机制的理解

【参考答案】

一个或两个。如果常量池中原来没有 xxx,就是两个。如果有就是一个。

String 是不可变类,Java 会分配一块常量池。通过以下代码解释下常量池的使用。

String str1 = "蜗牛666";

String str2 = "蜗牛666";

String newStr1 = new String("蜗牛666");
String newStr2 = new String("蜗牛666");

str1 这个变量接收了一个常量,于是存储到常量池中,执行到 str2 的赋值语句时,发现已经有相同的常量了,于是 str2 也指向了 str1 刚才那块内存空间。

newStr1 和 newStr2 是通过 new 语法创建的对象,在创建的过程中,Java 会先去常量池中查找是否已有 蜗牛666 对象,如果没有则在常量池中创建一个 蜗牛666 对象,然后在堆中创建一个 蜗牛666 的拷贝对象。

图片
 

8、Java 的封装特性都体现在哪些地方?

【考察点】

考察对 Java 面向对象的基础知识。

【参考答案】

  1. 通过包(package)的方式,把一个模块封装到一起,并由几个接口开放给使用方。使用方只能看到接口信息,而看不到接口实现。
  2. 通过访问权限控制的方式,实现信息隐藏。
  3. 通过 getter 和 setter 方法,实现了对类成员有条件的读取和修改。

9、谈谈你对双亲委派模式的理解?

【考察点】

考察对 Java 类加载机制的理解。

【参考答案】

双亲委派模式要求除了顶层的引导类加载器外,其余的类加载器都应当有自己的父类加载器

双亲委派的工作原理是,如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器。

如果父类加载器可以完成类加载任务,就成功返回,如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

你会发现,Java 类随着它的类加载器具备了带有优先级的层次关系,通过这种层级关系,可以避免类的重复加载,当父类已经加载了该类,就没必要子加载器再加载一次。

因此像 java.lang.String 这种 Java 核心 API,即便你同名,JVM 也会优先加载 rt.jar 里的,因为引导类加载器是最顶级的加载器。这样也避免了 Java 核心 API 被随意替换,保证了安全。

图片

后记

很多问题看似简短,背后能说道的东西其实很多,这就需要日积月累的沉淀,而非一朝一夕之功。面试题精选系列每一篇的题目不会超过 10 个,为的是读者朋友可以很好的消化内容,同时也能够深入了解相关的知识,希望对读者的面试能有帮助!


推荐阅读:

编程的本质是什么?

Java 基本类型的各种运算,你真的了解了么?

Java 面向对象一览

更多内容请收藏:Java for You

我是如何放弃 JSP,转向 REST 编程的

作者 蜗牛
2022年6月3日 14:31

记得大学搞编程的时候,比起研究数据结构,做算法题,我更喜欢搞 web 编程。因为做 web 是可以通过浏览器快速看到效果的,可视化的页面也能带给自己满足感。

我画了个图,读者朋友可以感受下,自己作为用户,请求自己代码编出来的页面,岂不是很有成就感?

rest

如果你作为用户来访问互联网资源,那么大概的过程是这样的:你在浏览器是录入  URL 或者点击一个超链接后,浏览器会请求 DNS 服务器解析这个 URL,返回域名映射的IP,然后通过 HTTP 请求这个 IP 对应的 web 资源,如果涉及到一些数据的查询,还会访问数据库服务器获取数据,然后把数据返回,web 服务器将数据和样式处理下,转成 web 资源,然后返回给浏览器,经过浏览器的渲染,你就能看到可视化的页面了。

但那时搞 web 编程还比较麻烦,什么 JSP,ASP,前端代码和后端代码杂糅在一起,就这么你离不开我我离不开你似的在 web 服务器上跑着,代码看上去不清爽,很多业务逻辑也没法被其它站点复用。

图片

假设现在有三个巨头企业,他们分别维护 baitu.com,kk.com,taopao.com 三个站点,这些站点向服务端的交互都是通过 JavaScript 客户端实现的。某一天三家合并了,那就会涉及很多业务互通,比如 baitu.com 站点下想访问 kk.com 的一些数据。

图片

这时候该怎么做呢?过去通用的解法是用 SOAP(Simple Object Access Protocol,简单对象访问协议),这是一种基于 XML 格式以及 HTTP 传输方式的数据交换协议。

图片

前端 JavaScript 是不能直接访问 SOAP 服务的,需要先访问到 baitu.com 对应的网站后台,然后由网站后台去访问 kk 提供的 SOAP 服务(要在之前网站后台直接访问数据库的逻辑上,抽取单独的服务层出来),然后再由 baitu.com 的网站后台对返回的数据进行渲染,将 HTML 等资源信息返回给前端。

那么这时候问题就来了,我在 baitu.com 上的一个前端页面上,一旦想加点 kk 才有的数据,我就必须得改 baitu.com 的网站后台,并且要重新接入 kk 提供的 soap 服务。这显然是一种低效的架构方式,相当影响研发效率。

那么有没有一种方式,我不需要经过 baitu 的网站后台,直接就能访问到 kk 的服务呢?页面上业务逻辑的处理,就不要放网站后台了,在 JavaScript 的客户端直接做掉,通过访问后端的某种服务获得业务处理的结果,然后基于网站后台存放的 HTML 和 CSS 来渲染页面。

图片

就像图示的这样,baitu 的 JavaScript 客户端就做两件事,访问后端服务获取业务逻辑处理的结果数据,将数据以 网站后台存放的 HTML 和 CSS 的要求展示出来。这样看来,网站后台就像个壳,只负责本站的 HTML、CSS 和 JavaScript 等静态资源,相关的业务逻辑处理就交给服务来提供。

这就是前后端分离的思想,前端静态资源和后端动态服务解耦。前端只关心 HTML 等前端代码,不涉及一行后端代码,后端只关心自己提供的服务,不涉及一行前端代码。

在这种思想的指引下,SPA(single page web application,单页面应用)就出现了。SPA 是单个 HTML 页面的 Web 应用程序,它在用户与应用程序交互时由 JavaScript 动态更新页面。其工作原理如图。

图片

浏览器客户端一开始会加载必需的 HTML、CSS 和 JavaScript,之后的所有的操作都在这张页面上完成,由 JavaScript 来控制,通过某种数据格式和服务端产生交互,获取返回结果。

这个时候,客户端就需要服务端提供的业务服务得是一个 API(应用程序访问接口),客户端可以直接发起请求,这时候 REST API 就派上用场了。

什么是 REST 呢?

REST 是 REpresentational State Transfer(表述性状态转移) 的首字母缩写,这名字什么鬼?好难理解的样子,不过它本身就源于国外一个博士的论文,论文嘛,大家都知道的,一般不太好理解。我这里画了个图,通过分拆的方式,帮助大家理解下:

图片

REST 是一种设计思想,它的核心是资源,可以理解成在 REST 的世界里,万物皆资源。

  • REpresentational(表述性):这是个形容词,它想要表达的意思是,资源可以用各种形式来进行表述,无论是 XML、JSON 还是 HTML,只要适合资源使用者,任何形式都可以。
  • State(状态):这是个名词,也是 REST 思想的本质。它告诉开发者,REST 关注的是资源当前的状态,而不是对资源采取的行为。无论资源的形式如何变化,它要表达的内容其实是统一的,该资源存在还是不存在,单个信息还是多个信息,都有哪些属性,这就是资源的状态。
  • Transfer(转移):这是个动词,它指转移资源,以某种表述性形式资源从一个应用转移到另一个应用。转移过程中,资源状态可能会有所变化。

在 REST 中,资源是通过 URL 进行识别和定位的。对资源的操作,是通过 HTTP 方法来定义的。HTTP 方法一般会映射到数据层的 CRUD 动作:

数据层动作 HTTP 方法 描述
Create POST 新建资源
Read GET 获取资源
Update PUT 或 PATCH 更新资源
Delete DELETE 删除资源

此映射不是严格限制,读者朋友可以根据实际情况灵活映射。

比如很多网站会维护用户的个人资料信息,如果用 REST 来设计相关操作的 API,可以这么设计:

操作项 URL HTTP 方法
新增个人资料 http://api.example.com/profile POST
查询个人资料 http://api.example.com/profile GET
修改个人资料 http://api.example.com/profile PUT
删除个人资料 http://api.example.com/profile DELETE

简单讲,REST 就是 URL 定位资源,HTTP 方法操作资源

图片

REST 的出现是对过去编程模式的重大颠覆,除了架构上客户端和服务端的解耦,前后端各司其职,也极大提升了开发团队的研发效率。希望我在编程模式上的变化和思考能对你有所启发。

原创不易,感谢读者朋友给我点赞,在看,评论和转发,也欢迎关注我,我会分享更多优质原创内容。你的每份支持,我都会当成 99 元的赞赏。

我是蜗牛,Java 后端开发,正在互联网上疯狂爬行,欢迎一起来爬,下期见。


推荐阅读:

五分钟快速构建第一个 Spring Boot 应用

Spring Boot 项目的这些文件都是干啥用的?

图片

更多内容请收藏:Java for You

Java 数组迭代你用对了吗?

作者 蜗牛
2022年5月29日 18:08

Java 数组迭代

Java 数组是我们学习或工作中常用到的数据结构,我们会经常写数组迭代的代码,但你的代码是最优雅的么?

本文介绍三种 Java 数组迭代的方式以及最佳实践。

1、概述

首先我们通过遍历数组值的基本方法,来迭代数组。其次我们用 Java 比较古老的方法实现,最后我们再使用 Java 8 的 Stream API 将数组转成迭代器。

除此之外,我们可以把这个技巧应用在字符串的处理上。

2、通过循环进行数组迭代

通过循环在数组上建立迭代逻辑,根据索引从数组中获取相应的值。

代码:

package cn.java4u.codebase.array.iterator;

/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */

public class ArrayIterate {

    public static void main(String[] args) {

        // string array
        String[] names = new String[] {"john""Amal""Paul"};         // iterating array over its values.         for(int index=0; index< names.length ; index++) {             System.out.println(names[index]);         }     } }

输出:

john
Amal
Paul

3、JDK 8 之前使用老方式进行数组迭代

JDK 8 之前,在数组转列表的时候,按照老方式我们通常使用  Arrays.asList() 方法先得到一个 list,然后使用 list.iterator() 方法得到一个迭代器,最后遍历迭代器里所有的值。

package cn.java4u.codebase.array.iterator;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class JavaArrayToIterableExample {

    public static void main(String[] args) {

        // string array
        String[] names = new String[]{"john""Amal""Paul"};

        // string array to list conversion
        List<String> namesList = Arrays.asList(names);

        // List to iterable
        Iterator<String> it = namesList.iterator();

        // printing each value from iterator.
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

输出:

john
Amal
Paul

4、使用 Java 8 Stream 进行数组迭代

上一小节中,我们是通过  Arrays.asList() 把数组转成列表,现在我们使用 Java 8 Stream API 也可以做到,只需要用 Arrays.stream() 替换就行,它接收数组并返回数组类型的数据流。

stream() 方法可以把数组转换为 Stream 对象,在较大的数组上使用并行执行能力,然后需要调用 Stream 对象上的 iterator() 方法将 Stream 转换为 Iterator

参考代码如下:

package cn.java4u.codebase.array.iterator;

import java.util.Arrays;
import java.util.Iterator;
import java.util.stream.Stream;

/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class JavaArrayToIterableExampleJava8 {

    public static void main(String[] args) {

        // string array
        String[] names = new String[] {"john""Amal""Paul"};

        System.out.println("多行打印的解决方案");
        // Convert string array to Stream<String>
        Stream<String> namesList = Arrays.stream(names);

        // Stream to iterable
        Iterator<String> it = namesList.iterator();

        // printing each value from iterator.
        while(it.hasNext()) {
            System.out.println(it.next());
        }

        // singel line
        System.out.println("\n单行打印的解决方案");
        Arrays.stream(names).iterator().forEachRemaining(name -> System.out.println(name));
    }
}

多行和单行解决方案提供了相同的输出。如果你打算在实际项目中使用,最好用单行语句,因为这显得更像专家,并且利用了 Stream 的优势!

多行打印的解决方案
john
Amal
Paul

单行打印的解决方案
john
Amal
Paul

5、字符串的应用

如果你理解了上边的内容,那么在字符串上应用迭代能力就很简单了。

比如我们有个字符串,用空格分隔了不同的内容,假设是:1 2 3 4 5 6,我们需要把每个内容分别打印出来,此时就可以应用 Java 8 Stream 来进行迭代处理。

package cn.java4u.codebase.array.iterator;

import java.util.Arrays;
import java.util.Iterator;
import java.util.stream.Stream;

/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class JavaStringToIterableCase {
    public static void main(String[] args) {

        // string
        String numbers = "1 2 3 4 5 6";

        // string to string array
        String[] numbersArray = numbers.split(" ");

        System.out.println("多行打印的解决方案");
        // Convert string array to Stream<String>
        Stream<String> numbersList = Arrays.stream(numbersArray);

        // Stream to iterable
        Iterator<String> it = numbersList.iterator();

        // printing each value from iterator.
        while(it.hasNext()) {
            System.out.println(it.next());
        }

        // singel line
        System.out.println("\n单行打印的解决方案");
        Arrays.stream(numbersArray).iterator().forEachRemaining(name -> System.out.println(name));
    }
}

输出:

多行打印的解决方案
1
2
3
4
5
6

单行打印的解决方案
1
2
3
4
5
6

6、总结

本文介绍了如何对数组做迭代的三种方法,分别是循环方式、集合迭代器方式以及流迭代器方式,最后把流迭代器方式应用在了字符串的处理上。

推荐阅读:

1120页的Leetcode算法刷题笔记,完整版PDF开放下载!

《Java 工程师成神之路》.pdf

更多内容请收藏:Java for You

【Java基础教程13】Java异常处理进阶篇

作者 蜗牛
2022年5月29日 18:00

Java异常处理

前言

《Java 异常处理一览 | 基础篇》介绍了 Java异常处理的一些基本操作,本文介绍下异常处理的一些进阶操作!

try-with-resources 语句

我们在对一些资源进行操作时,经常会有固定的写法:

  • try 中打开资源
  • finally 中关闭资源

比如下面这个程序:

static String readFirstLineFromFileWithFinallyBlock(String path)
                                                     throws IOException 
{
    
    BufferedReader br = new BufferedReader(new FileReader(path));
    
    try {
        return br.readLine();
        
    } finally {
        br.close();
        
    }
}

程序示例是从文件中读取第一行。它使用 BufferedReader 实例从文件中读取数据,这是一个必须在程序完成后关闭的资源。

在 Java SE 7 之前,对资源的操作只能通过上边比较繁琐的代码实现,也就是使用 finally 块确保关闭资源。

但 Java SE 7 之后,使用 try-with-resources 语句就可以做到无需在 finally 块中显式关闭资源,不管 try 语句是正常完成还是异常阻断,资源都会被自动关闭!写法如下:

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
                   new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

你会发现,try 语句多了一对圆括号,圆括号里能塞表达式,表达式里其实就是对资源的声明

多资源处理

这时候你可能会问,如果 try 中我有多个资源要声明怎么办呢?

可以用分号分割。

像下面这样:

public static void writeToFileZipFileContents(String zipFileName,
                                           String outputFileName)
                                           throws java.io.IOException {

    java.nio.charset.Charset charset =
         java.nio.charset.StandardCharsets.US_ASCII;
    java.nio.file.Path outputFilePath =
         java.nio.file.Paths.get(outputFileName);

    // Open zip file and create output file with 
    // try-with-resources statement

    try (
        java.util.zip.ZipFile zf =
             new java.util.zip.ZipFile(zipFileName);
        java.io.BufferedWriter writer = 
            java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
    ) {
        // Enumerate each entry
        for (java.util.Enumeration entries =
                                zf.entries(); entries.hasMoreElements();) {
            // Get the entry name and write it to the output file
            String newLine = System.getProperty("line.separator");
            String zipEntryName =
                 ((java.util.zip.ZipEntry)entries.nextElement()).getName() +
                 newLine;
            writer.write(zipEntryName, 0, zipEntryName.length());
        }
    }
}

进入 try 代码块的执行后,无论是正常还是异常,当代码块终止时,BufferedWriterZipFileclose 方法会依次被调用,从而达到自动关闭资源的效果。

注意:资源的 close 方法的调用顺序和它创建顺序相反,这个也好理解,你把 try 中涉及的所有资源想象依次放到一个比较窄的池子里,那这就像队列一样,先进后出,先创建的最后关闭资源。

原理解析

此时,你可能会问,为什么示例中的资源会被自动关闭资源呢?

最直接的原因就是,他们都有关闭资源的方法

根本原因是,它们都实现了 AutoCloseable 的接口,具备了 close 的能力。

图片

见名知意,AutoCloseable 也表明了实现它接口的对象,都具备自动关闭自己的能力。看注释,since 1.7,你也能知道,这个能力的确是从 Java 7 开始的。

所以,如果你有自定义资源,记得要实现 AutoCloseable

推荐阅读:

1120页的Leetcode算法刷题笔记,完整版PDF开放下载!

《Java 工程师成神之路》.pdf

更多内容请收藏:Java for You

【Java基础教程11】Java核心类库一览

作者 蜗牛
2022年5月29日 17:55
Java核心类库

今天我们聊聊Java核心类库,阅读本文你将收获:

Java核心类库

类库与 JAR 文件

什么是类库

我们知道,在面向对象的程序设计里,一个类是可以调用另外一个类的方法,只要把被调用的那个类引入到 classpath 下就可以。

一个类当然好搞,但如果是很多类,都会被其他类重复使用到,并且可能有多个工程,其他开发者也需要,那么按类这个维度去加入 classpath 显然会很麻烦。

这种情况就需要把这些可以被重复使用的类打包,统一提供给使用方。这种打包好的类,就是类库(Class Library)。

类库是类的集合,可以被重复使用。

什么是 JAR 文件

类库只是一种概念,不同程序设计语言,表现形式不同。在 Java 中,一般以 JAR 文件的方式提供类库

什么是 JAR 文件呢?

JAR (Java ARchive,Java 归档)是一种软件包文件格式,通常会聚合大量的 Java 类文件、相关的元数据和资源(文本、图片等)文件到一个文件,以便分发到 Java 平台应用软件或库。

JAR 文件就是 JAR 这种格式下的归档文件,以 ZIP 格式构建,以 .jar 为文件扩展名。用户可以使用 JDK 自带的 jar 命令创建或提取 JAR 文件。

JAR 文件创建和提取

我们演示下 JAR 文件的创建和提取过程。

假设你 Java 环境已经安装好,我们创建一个演示目录并进入:

mkdir jartest
cd jartest

创建一个 Java 文件,命名为 A.java:

vi A.java

编辑 Java 文件:

public class A {


}

再创建一个 Java 文件,命名为 B.java:

vi B.java

编辑 Java 文件:

public class B {


}

编译 Java 代码:

javac A.java B.java

我们发现已经生成了 .class 文件:图片

使用 JDK 自带的 jar 命令创建 JAR 文件:

jar cvf ab.jar A.class B.class

JAR 文件生成成功!

图片

我们去一个新的目录提取下 JAR 文件!

mkdir xvf
cd xvf
jar xvf ../ab.jar

解压会得到以下内容:图片

我们看下 MANIFEST.MF 文件内容:

$ cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: 1.8.0_281 (Oracle Corporation)

Manifest 文件看着是创建 JAR 文件自动生成的,它起什么作用呢?

在 Java 平台中, Manifest 文件是 JAR 归档中所包含的特殊文件。Manifest 文件被用来定义扩展或文件打包相关数据。Manifest 文件是一个元数据文件,它包含键 – 值对(英语:Attribute–value pair)数据。如果一个 JAR 文件被当成可执行文件,那么其中的 Manifest 文件需要指出该程序的主类文件。通常 Manifest 文件的文件名为 MANIFEST.MF

JAR 文件的使用

假设 ab.jar 功能强大,即将提供给其他开发者使用,那怎么接入呢?

我们新建个目录,新建个使用类,尝试使用下 A 这个类:

mkdir usejar
cd usejar
vi UseJarDemo.java

编辑 Java 文件,我们 new 一个 A,表明要使用它:

public class UseJarDemo {

    public static void main(String[] args) {

        A a = new A();
    }
}

保存退出,尝试编译:

javac UseJarDemo.java
图片

我们看到编译没通过,它找不到 A 这个类,为什么呢?

因为我们没有把 A 这个类加入到 CLASSPATH 里!

既然我们学习了 JAR,我们就不像以前把单个类往类路径里加了,我们以 JAR 的形式添加,看下能不能正常使用!

打开环境配置文件(我的是 macOS 系统),使用命令vi ~/.bash_profileCLASSPATH 后边追加 ab.jar 所在的路径,然后 source ~/.bash_profile 生效一下配置文件,

追加前

$ cat ~/.bash_profile
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$JAVA_HOME/bin:$PATH

追加后

$ cat ~/.bash_profile
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:/Users/woniu/jartest/ab.jar
export PATH=$JAVA_HOME/bin:$PATH

此时我们再编译一下 UseJarDemo.java

javac UseJarDemo.java

非常顺利,没有报错!说明我们成功用到了 JAR 中的 A!

$ ls
UseJarDemo.class UseJarDemo.java

Java 核心类库

上一小节我们把自己创建的 JAR 文件添加到 CLASSPATH 路径下的时候,眼尖的你可能发现我们的通用配置CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar 里有两个 jar 非常醒目。

它们就是 dt.jartools.jar,这俩都在 $JAVA_HOME/lib 下。这是 Java 官方把很多功能强大的类打包,做成类库,随着 JDK 一起发布。

dt.jar

dt 是 DesignTime 的缩写,翻译过来就是设计时。

为什么叫设计时呢?

因为 dt.jar 是面向图形用户界面(GUI)场景的,使用它你可以在开发环境通过添加控件、设置控件或窗体属性来设计你的图形化的应用程序。

图片

dt.jar 是设计时环境的类库,主要是 swing 包。因此如果你的开发场景不涉及 GUI,是可以不引入这个包的。

tools.jar

tools.jar 是工具类库,运用在编译和运行以及其他场景。我们经常用的 javac、java 命令文件都很小,一般几十上百 KB。这是因为它们实际上只是一层代码的封装,这些工具的实现所要用到的类库都在 tools.jar 中。

图片

图片

观察 tools.jar ,我们会发现很多文件和 bin 目录下的可执行文件是有相对性的。除此以外,也提供了像 Applet 和 RMI 这样的文件,支持其他的一些功能。

rt.jar

rt.jar 不同于 dt.jartools.jar 的位置,它位于 $JAVA_HOME/jre/lib 下。

rt 是 RunTime 的缩写,翻译过来就是运行时。

为什么叫运行时呢?

因为它包含了所有已编译的类文件,包括引导类以及来自核心 Java API 的所有类,是 Java 运行时环境中所有核心 Java 类的集合。

rt.jar 是运行时环境的类库,当你编写程序用到很多系统类,比如 StringSystem 这些类,其实都来自这个类库。JVM 在运行时从 rt.jar 访问所有这些类。如果类路径中没有包含 rt.jar,就无法访问 java.lang.String,java.util.ArrayList 和 Java API 中的所有其他类。

图片

由于 rt.jar 中的所有类都是 JVM 已知,当 JVM 加载这些类时,会用单独的引导类加载器(Bootstrap ClassLoader)进行加载。这样做的好处就是,不需要做太多的基本安全检查,提升了性能。

有朋友可能会问,我自己定义的类路径和 rt.jar 冲突会发生什么呢?会不会加载我自己定义的类呢?比如我也定义了一个类 String,路径是 java.lang.String。

答案是不会!这就要提到 Java 虚拟机的类加载机制了。

Java 有四种类加载器,分别是

  • 引导类加载器 Bootstrap ClassLoader,负责加载 $JAVA_HOME/jre/lib/rt.jar 下的类。
  • 扩展类加载器 Extension ClassLoader,负责加载下 $JAVA_HOME/jre/lib/ext/*.jar 的类。
  • 系统类加载器 System ClassLoader,负责加载 $CLASSPATH 下的类。
  • 用户自定义加载器 User Defined ClassLoader。

Java 虚拟机对类文件的加载,采用的是双亲委派模式。双亲委派模式要求除了顶层的引导类加载器外,其余的类加载器都应当有自己的父类加载器

但这里请注意,叫法上虽然是父类,实际上他们之间并非继承关系,而是采用组合关系来复用父类加载器的相关代码。

图片

双亲委派的工作原理是,如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的引导类加载器。

如果父类加载器可以完成类加载任务,就成功返回,如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

你会发现,Java 类随着它的类加载器具备了带有优先级的层次关系,通过这种层级关系,可以避免类的重复加载,当父类已经加载了该类,就没必要子加载器再加载一次。

因此像 java.lang.String 这种 Java 核心 API,即便你同名,JVM 也会优先加载 rt.jar 里的,因为引导类加载器是最顶级的加载器。这样也避免了 Java 核心 API 被随意替换,保证了安全。

常用的 Java 类库 API

什么是 API

我们前边多次提到 Java API,那么什么是 API 呢?

API 的全称是 Application Programming Interface,翻译过来就是 应用程序接口

假如我写了一个类,可以对输入文本进行翻译,这个类非常稳定且功能好用,如果你的项目中也需要这么一个功能。那你就不需要自己编写代码,直接把我的类拿来用就可以。但我又不想让别人看到内部实现,想要保护版权,怎么办呢?

这时候我可以将我的类编译,并附带一个文档,告诉你我的类怎么使用,有哪些方法,你只要按照文档说明来调用就可以。既节省了你编码实现的时间,也保护了我的版权。比如文本翻译的方法:

String translate(String text, String language)

像这种描述类的使用方法,就叫做 API。

Java API 也有说明文档,比如 Java SE 8:https://docs.oracle.com/javase/8/docs/api/index.html

图片

常用的 API

介绍 Java 核心类库中常用的 API。

包名 包说明 API API 说明
java.lang java 核心包,覆盖 Java 编程的基础类,JVM 自动导入无需手动导包。 Object#equals(Object obj) 判断其他某个对象是否与此对象“相等”
Object#hashCode() 获取调用对象的哈希码值
Object#toString() 获取调用对象的字符串形式
String#length() 获取字符串的长度
java.util java 工具包,覆盖集合类和工具类。 Date() 根据当前系统时间来构造对象。
Collection#add(E e) 向集合里添加对象
List#get(int index) 从集合中获取指定位置元素
Queue#offer(E e) 将一个对象添加至队尾
Set#iterator() 用于获取当前集合中的迭代器对象,可以取出每个元素
Map#put(K key,V value); 将key-value对存入Map,若集合中已经包含该key,则替换该Key所对应的Value,返回值为该Key原来所对应的Value,若没有则返回null(增加和修改)
java.io java 输入输出包,通过文件系统、数据流和序列化提供系统的输入和输出。 File(String Pathname) 根据参数指定的路径来构造对象
File#createNewFile() 用于创建新的空文件
java.net java 网络包,覆盖网络编程类。 Socket#close() 关闭 Socket
java.sql java 数据 API 包,覆盖操作数据库的所有类和接口。 Connection#createStatement() 创建向数据库发送 SQL 的语句

总结

本文介绍了类库的概念以及 JAR 文件的使用,重点讲解了 Java 中三个常见的 JAR 的概念,由来和作用,同时提到了 Java 的类加载机制是双亲委派模式,最后介绍了 API 的概念以及 Java 类库中常用到的一些 API。看完这篇文章,想必你对 Java 核心类库有了更深的了解。

好啦,本期分享就到这里,如果各位喜欢我的分享,请务必三连,点赞,在看,收藏,关注我,这会对我有非常大的帮助。

我们下期再见。

推荐阅读:

1120页的Leetcode算法刷题笔记,完整版PDF开放下载!

《Java 工程师成神之路》.pdf

更多内容请收藏:Java for You

【Java基础教程10】Java面向对象一览

作者 蜗牛
2022年5月29日 17:50
Java面向对象

本文大纲:Java面向对象

前言

学 Java 的朋友都知道,Java 是一门典型的面向对象的高级程序设计语言,但有些朋友可能不清楚面向对象在 Java 中是怎么体现的。这篇文章就向大家分享下Java面向对象方面的一些知识。

Java 语言简介

Java 语言特点

首先我们看下 Java 的语言特点,如图所示。

图片

Java 是纯粹的面向对象语言,它因统一的字节码文件和差异化的 JDK 而具有平台无关的特性。

Java 内置丰富的类库,使开发者效率大为提升。它支持 web,广泛应用于各大互联网企业的网站后台,像阿里美团都在使用。

Java 的安全性也很出众,通过沙箱安全模型保证其安全性,能够有效防止代码攻击。

Java 也具备很强的健壮性,比如它是强类型的,支持自动化的垃圾回收器,有完善的异常处理机制和安全检查机制。

与 C++ 比较

同样是面向对象的编程语言,Java 和 C++ 存在异同。

比较点 C++ Java
语言类型 编译型语言 解释编译混合型语言
执行速度
是否跨平台
面向对象 面向对象和面向过程混合 纯面向对象
指针
多继承 支持 不支持
内存管理 手动 自动

从语言类型上看,C++ 的代码编译好,就能被计算机直接执行,它是编译型语言,而 Java 经过 javac 把 java 文件编译成 class 文件后,还需要 JVM 从 class 文件读一行解释执行一行,它是解释编译混合型语言。也就是中间多了 JVM 这一道,Java 也具备了跨平台特性,而 C++ 就没有这个优势。

从面向对象的角度上看,C++ 是在 C 的基础上的新的探索和延伸,因此它是面向对象和面向过程混合的,而 Java 就是纯粹的面向对象。

此外,C++ 有指针的概念,Java 没有。C++ 支持多继承,Java 不支持。C++ 需要手动进行内存管理,Java 通过垃圾回收机制实现了内存的自动管理。

面向对象思想

我们总在提面向对象,那面向对象究竟是个什么东西呢?在面向对象出现之前的面向过程又是怎么回事呢?

其实无论是面向对象还是面向过程,都是我们在编程时解决问题的一种思维方式。

只是在最初,人们分析解决问题的时候,会把所需要的步骤都列出来,然后通过计算机中的函数把这些步骤挨个实现,这种过程化的叙事思维,就是面向过程思想。

你比如,把一头大象放进冰箱,通常会怎么做呢?

我们的习惯性思维是会分为三步,第一步,把冰箱门打开,第二步,把大象推进去,第三步,把冰箱门关闭(假设大象很乖,冰箱很大,门能关住)。

图片

这种方式固然可行,但当场景发生变化时,比如大象变成猪,冰箱变成衣柜,类似的步骤用面向过程编码的话就要再写一遍。这样就导致代码开发变成了记流水账,久而久之就成为面条代码。

我们仔细分析面向过程的这些步骤,会发现都是命令式的动宾结构:开冰箱门,推大象,场景切换下就是开衣柜门,推猪。你会发现从这两种场景下是可以找到共性的,就是冰箱门和衣柜门都有打开和关闭的特点,大象和猪都能走路,所以能被人推进去。

当我们的视角不再是流程,而是操作对象的时候,冰箱门和衣柜门都可以抽象成门,有打开和关闭的特点,大象和猪都可以抽象成动物,有走路的特点。按这个思路,我们可以把这件事简化成主谓结构:门打开,动物走进去,门关闭。

这种把事情分解成各个对象,描述对象在整个事情中的行为,就是面向对象思想。

你会发现,面向过程更讲事情的步骤,面向对象更讲对象的行为

面向对象可以基于对象的共性做抽象,为软件工程的复用和扩展打好了坚实的基础。这也是为什么在很多大型软件开发选型上,大多会使用面向对象语言编程。

面向对象基础

Java 作为纯面向对象语言,我们有必要了解下面向对象的基础知识。

面向对象有四大特征,是抽象封装继承多态。也有很多人认为是三大特征,不包括抽象,但我觉得抽象才是面向对象思想最为核心的特征,其他三个特征无非是抽象这个特征的实现或扩展。

我总结了下这四大特征在面向对象领域分别解决了什么问题,再逐一介绍:

  • 抽象:解决了模型的定义问题。
  • 封装:解决了数据的安全问题。
  • 继承:解决了代码的重用问题。
  • 多态:解决了程序的扩展问题。

抽象

抽象是面向对象的核心特征,良好的业务抽象和建模分析能力是后续封装、继承和多态的基础。

面向对象思维中的抽象分为归纳演绎两种。

归纳是从具体到本质,从个性到共性,将一类对象的共同特征进行归一化的逻辑思维过程。比如我们把见到的像大象,老虎,猪这些能动的有生命的对象,归纳成动物。

演绎是从本质到具体,从共性到个性,将对象逐步形象化的过程。比如从生物到动物,从动物到鸟类。演绎的结果不一定是具体的对象,也可以是像鸟类这种抽象结果,因此演绎仍然是抽象思维,而非具象思维。

Java 中的 Object 类是任何类的默认父类,是对万物的抽象。这就是我们常说的:万物皆对象

看一看 java.lang.Object 类的源码,我们基本能看到 Java 世界里对象的共同特征。

图片

getClass() 说明了对象是谁,toString() 是对象的名片,clone() 是繁殖对象的方式, finalize() 是销毁对象的方式,hashCode()equals() 是判断当前对象与其他对象是否相等的方式,wait()notify() 是对象间通信与协作的方式。

类的定义

除了 JDK 中提供的类之外,我们也可以基于自己业务场景的抽象定义类。

我们看下 Java 语法中的 class(类)是怎么构成的。

以下是概览图,我们按图介绍。

图片

我们先关注图中的黄色区块,在 Java 里就叫 class(类)。

好比一个事物有属性和能力一样,比如人有名字,人能吃饭。对应到 Java class 里就是变量和方法,即红色区块和紫色区块。

变量分为成员变量静态变量局部变量三种,方法分为构造方法实例方法静态方法三种。

我们举个例子来说明下,假设全世界的面包数量就 100 个,并且生产已经停滞,而且只有蜗牛和小白两个人能吃到,我们就可以按以下的代码来描述这两个人吃面包的过程以及面包的情况。

package cn.java4u.oo;


/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class Person {

    /**
     * [成员变量]需要被实例化后使用,每个实例都有独立空间,通过 对象.成员变量名 访问
     * 名字
     */
    String name;


    /**
     * [静态变量]用 static 修饰,无需实例化即可使用,每个实例共享同一个空间,通过 类名.静态变量名 访问
     * 面包数量
     */
    static int breadNum;

    /**
     * [方法]
     * 吃一个面包
     *
     * @param num 方法入参,要吃面包的个数
     */
    void eatBread(int num) {

        //  num 是[局部变量]
        breadNum = breadNum - num;

        System.out.println(name + "吃了 " + num + " 个面包,全世界的面包还剩 " + breadNum + " 个!");
    }

    /**
     * [构造方法]
     * 参数为空
     */
    public Person() {
    }

    /**
     * [构造方法]
     *
     * @param name 此为构造方法的输入参数,和成员变量有关
     */
    public Person(String name) {
        this.name = name;
    }

    /**
     * [静态方法]
     */
    static void testStaticMethod() {

        // 通过构造方法,初始化名字叫蜗牛的人
        Person woniu = new Person("蜗牛");

        // 通过构造方法,初始化名字叫小白的人
        Person xiaobai = new Person("小白");

        // 假设全世界的面包数量就 100 个,并且生产已经停滞
        Person.breadNum = 100;

        // 蜗牛吃五个面包
        woniu.eatBread(5);

        // 小白吃六个面包
        xiaobai.eatBread(6);

        // 打印成员变量和静态变量的值
        System.out.println(woniu.name + "和" + xiaobai.name + "吃饱后,世界只剩 " + Person.breadNum + " 个面包了!");

    }
}

变量

首先定义了一个名字叫 Person 的类,表示人,然后定义了一个成员变量 name ,表示人的名字。成员变量也叫实例变量,实例变量的特点就是,每个实例都有独立的变量,各个实例之间的同名变量互不影响。

其次定义了一个静态变量 breadNum ,表示面包的数量,静态变量用 static 修饰。静态变量相对于成员变量就不一样了,它是共享的,所有实例会共享这个变量。

方法

再接着定义了一个返回值为空,只有一个入参的方法 eatBread(int num) ,方法入参 num 作为局部变量参与了内部的运算,通过和它的运算,静态变量breadNum 的值得到了更新,并打印了一行操作信息。方法的语法结构如下:

修饰符 返回类型 方法名(方法参数列表) {
    方法语句;
    return 方法返回值;
}

另外定义了 Person 的构造方法,你会发现构造方法和实例方法的区别就在于它是没有返回值的,因为它的目的很纯粹,就是用来初始化对象实例的,和 new 搭配使用,所以它的方法名就是类名,它的入参也都和成员变量有关。

到这里,你会发现 Java 方法的返回值并不是那么重要,甚至没有都可以!是的,Java 方法签名只包括名称和参数列表,它们是 JVM 标识方法的唯一索引,是不包含返回值的,更不包括各种修饰符或者异常类型。

请注意,任何 class 都是有构造方法的,即便你代码里不写,Java 也会在编译 class 文件的时候,默认生成一个无参构造方法。但是只要你手动定义了构造方法,编译器就不会再生成。也就是说如果你仅定义了一个有参的构造方法,那么编译后的 class 是不会有无参构造方法的。

最后就是静态方法了,名字叫testStaticMethod ,方法内部我们先用 new 的语法调用构造方法,初始化了蜗牛和小白的Person 对象。这两个对象就是 Person 这个类的实例,这两个实例都有独立空间,name 这个成员变量也只能在被实例化后使用,可以通过 对象.成员变量名 访问。

接着我们通过 Person.breadNum 也就是 类名.静态变量名  的方式,更新了面包数量这个值。你会发现 breadNum 这个静态变量无需实例化就能使用,因为就这个变量而言,Person 的每个实例都会共享同一个空间。这意味着,每个实例的修改,都会影响到这个变量值的变化。

图片

然后我们通过调用方法  eatBread 并传参的方式,影响到了面包数的值。

package cn.java4u.oo;

/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class MainTest {

    public static void main(String[] args) {


        // 静态方法,通过 类名.静态方法名 访问
        Person.testStaticMethod();
    }
}

最后我们新定义一个触发调用的入口函数,通过 Person.testStaticMethod() 这样 类名.静态方法名 的方式就能访问到静态方法了。

抽象类与接口

抽象类顾名思义,就是会对同类事物做抽象,通常包括抽象方法、实例方法和成员变量。被抽象类和抽象类之间是 is-a 关系,这种关系要符合里氏替换原则,即抽象类的所有行为都适用于被抽象类,比如大象是一种动物,动物能做的事,大象都能做。代码定义也很简单,就是在 class 和抽象方法上加 abstract 修饰符。

package cn.java4u.oo;

/**
 * 抽象类
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public abstract class AbstractClass {

    String name;

    /**
     * 实例方法
     *
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 抽象方法-操作
     *
     * @return 结果
     */
    public abstract String operate();
}

如果一个抽象类只有一个抽象方法,那它就等于一个接口。接口是要求被普通类实现的,接口在被实现时体现的是 can-do 关系,它表达了对象具备的能力。鸟有飞的能力,宇宙飞船也有飞的能力,那么可以把飞的能力抽出来,有单独的一个抽象方法。代码定义也比较简单,class 的关键字用 interface 来替换。

package cn.java4u.oo;

/**
 * 可飞翔
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public interface Flyable {


    /**
     * 飞
     */
    void fly();
}

内部类

在 Java 源代码文件中,只能定义一个类目与文件名完全一致的公开类。如果想在一个文件里定义另外一个类,在面向对象里也是支持的,那就是内部类。

内部类分为以下四种:

  • 静态内部类:static class StaticInnerClass {}
  • 成员内部类:private class InstanceInnerClass {}
  • 局部内部类:class MethodClass {} ,定义在方法或者表达式内部
  • 匿名内部类:(new Thread() {}).start();

示例代码如下:

package cn.java4u.oo.innerclass;

/**
 * 内部类演示
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class InnerClassDemo {

    /**
     * 成员内部类
     */
    private class InstanceInnerClass {}

    /**
     * 静态内部类
     */
    static class StaticInnerClass {}

    public static void main(String[] args) {

        // 两个匿名内部类
        (new Thread() {}).start();
        (new Thread() {}).start();

        // 方法内部类
        class MethodClass {}

    }
}

编译后得到的 class 文件如下:图片

我们会发现,无论什么类型的内部类,都会编译生成一个独立的 .class 文件,只是内部类文件的命名会通过 $ 连接在外部类后面,如果是匿名内部类,会使用编号来标识。

类关系

关系是指事物之间有没有单向或者相互作用或者影响的状态。

类和类之间的关系分为 6 种:

  • 继承:extends(is-a)
  • 实现:implements(can-do)
  • 组合:类是成员变量(contains-a)
  • 聚合:类是成员变量(has-a)
  • 依赖:单向弱关系(使用类属性,类方法、作为方法入参、作为方法出参)
  • 关联:互相平等的依赖关系(links-a)

序列化

内存中的数据对象只有转换为二进制流才可以进行数据持久化网络传输

将数据对象转换成二进制流的过程称为对象的序列化(Serialization)。

将二进制流恢复为数据对象的过程称为反序列化(Deserialization)。

常见的序列化使用场景是 RPC 框架的数据传输。

常见的序列化方式有三种:

  1. Java 原生序列化。特点是兼容性好,不支持跨语言,性能一般。
  2. Hessian 序列化。特点是支持跨语言,性能高效。
  3. JSON 序列化。特点是可读性好,但有安全风险。

封装

封装是在抽象基础上决定信息是否公开,以及公开等级,核心问题是以什么样的方式暴露哪些信息。

抽象是要找到成员和行为的共性,成员是行为的基本生产资料,具有一定的敏感性,不能直接对外暴露。封装的主要任务是对成员、数据、部分内部敏感行为实现隐藏

对成员的访问与修改必须通过定义公共的接口来进行,另外某些敏感方法或者外部不需要感知的复杂逻辑处理,一般也会进行封装。

像智能音箱,与用户交互的唯一接口就是语音输入,封装了内部的实现细节和相关数据。

设计模式七大原则之一的迪米特法则也说明了封装的要求,A 接口使用 B 接口,对 B 知道的要尽可能少

包(package)这个名称就很明显体现了封装的含义,它能起到把一个模块封装到一起,并由几个接口开放给使用方。使用方只能看到接口信息,而看不到接口实现。另外包解决重名问题,相同类名在相同路径下是不允许的,切换包路径就可以起相同的类名。

访问权限控制

我们编写的程序要想让使用方,能看到一些信息,又不能看到另外一些信息,这就涉及到信息隐藏了。

信息隐藏是面向对象程序设计的重要特点之一,它可以防止类的使用者意外损坏数据,对任何实现细节所作的修改不会影响到使用该类的其它代码,也使类更易于使用。

那在 Java 里,实现信息隐藏的就是访问权限控制机制了。Java 的访问权限控制有 4 个访问修饰符:publicprotectedprivate 和缺省。可以使用这四个访问修饰符修饰类的成员,它们在不同位置的可访问性如下表所示。

位置\访问修饰符 public protected 缺省 private
本类 可以 可以 可以 可以
本包 可以 可以 可以 不可以
子类 可以 可以 不可以 不可以
所有 可以 不可以 不可以 不可以

你会发现 public 不受任何限制,本类和非本类都可以随意访问(全局友好)。protected 本类及其子类可以访问(父子友好),同一个包中的其它类也可以访问(包内友好)。而缺省的时候,只有相同包中的类可以访问(包内友好)。private 只有本类可以访问,其余都不可以(类内友好)。

图片

除了为类成员添加访问权限控制外,也可以在定义类的时候,为类添加访问修饰符,对类进行访问权限控制。不过对类使用的访问修饰符只有 public 和缺省两种,访问范围也分别是全局友好和包内友好。

getter 与 setter

为了让类成员不对外直接暴露,我们经常把成员变量的访问权限设置成 private,而成员值的访问与修改使用相应的 getter/setter 方法。而不是对 public 的成员进行读取和修改。

package cn.java4u.oo.packagedemo;

/**
 * getter 和 setter 演示
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class GetterSetterDemo {

    /**
     * 成员变量私有化
     */
    private String name;

    /**
     * 公开方法获取成员变量值
     *
     * @return 名称
     */
    public String getName() {
        return name;
    }

    /**
     * 公开方法设置成员变量值
     *
     * @param name 名称
     */
    public void setName(String name) {
        this.name = name;
    }
}

继承

类继承

class 了解之后,我们考虑一个问题。如果两个 class,它们的变量和方法基本相同,仅仅是其中一个 class 会有一些自己特有的变量和方法,那么相同的那些变量和方法真的需要在两个 class 里都写一遍么?

比如一个表示学生的 class Student ,它相对于 class Person 只是多了一个分数 score  的成员变量,那还需要像下面这样,把 name 字段也定义一下么?

/**
 * 学生
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class Student {

    /**
     * 名字
     */
    String name;

    /**
     * 分数
     */
    int score;

}

这很明显带来了代码重复使用的问题!那能不能在 Student 中不写重复代码?

Java 里的继承这时候就派上用场了,继承是面向对象编程的一种强大机制,能够让子类继承父类的特征和行为,使得子类对象能够具有父类的实例变量和方法。

子类继承父类,父类派生子类。父类也叫基类,子类也叫派生类。

通常来讲,类的层次划分总是下一层比上一层更具体,并且包含上一层的特征,这样下层的类就能自动享有上层类的特点和性质。继承就是派生类自动地共享基类中成员变量和成员方法的机制。

在 Java 中,通过 extends 关键字实现继承,并且所有的类都是继承于 java.lang.Object ,所以这就是万物皆对象在 Java 里的真实写照。你可能会疑惑,自定义的类并没有 extends 关键字为什么还能继承 Object 呢?这是因为这个类在 java.lang 包里,Java 已经默认支持了。

package cn.java4u.oo;

/**
 * 学生
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class Student extends Person {

    /**
     * 分数
     */
    int score;

}

知道了继承的基础概念后,我们看下继承有啥作用?

首先,继承是能够自动传播代码和重用代码的有力工具。它能在已有类上扩充新类,减少代码的重复冗余,也因为冗余度降低,一致性就得到了增强,从而提升了程序的可维护性。

其次,继承可以清晰体现出类与类之间的层次结构关系,提升了代码的可读性。

另外,继承是单方向的,即派生类可以继承和访问基类成员,但反过来就不行。而且 Java 只允许单一继承,也就是一个派生类不能同时继承多个基类,这和 C++ 是不同的。

在使用继承的时候,还要考虑到基类成员的访问控制权限。可以参考封装那块内容的访问权限控制介绍。

子类实例化过程

特别要说明的是,父类的构造方法是不能被子类继承的,即便它是 public 的。父类的构造方法负责初始化属于它的成员变量,而子类的构造方法只需考虑自己特有的成员变量即可,不必关注父类状况。

package cn.java4u.oo.inherit;

/**
 * 定义父类
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class Parent {


    /**
     * 构造方法
     */
    public Parent() {

        System.out.println("这是父类 Parent 的构造方法");
    }
}

package cn.java4u.oo.inherit;

/**
 * 定义子类
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class Child extends Parent {

    /**
     * 构造方法
     */
    public Child() {

        System.out.println("这是子类 Child 的构造方法");

    }
}

package cn.java4u.test;

import cn.java4u.oo.inherit.Child;

/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class InheritTest {

    public static void main(String[] args) {

        Child child = new Child();
    }
}

因此,在实例化子类的对象时,Java 先是执行父类的构造方法,然后执行子类的构造方法。如果父类还有更上级的父类,就会先调用更高父类的构造方法,再逐个依次地将所有继承关系的父类构造方法全部执行。如果父类的构造方法执行失败,则子类的对象也将无法实例化。

上边的代码运行后,会输出:

这是父类 Parent 的构造方法
这是子类 Child 的构造方法

this 与 super

如果调用父类构造方法涉及到有参构造方法,可以使用 super 关键字来调用父类构造方法并传递参数。

说的 super,它还有一个能力,就是父类和子类的成员如果同名了,子类中默认只能访问自己的那个成员,想要访问父类成员,就可以通过 super.成员名 的语法实现。但这有个前提,就是父类的这个成员不能是 private 的。

super 相对的关键字是 thissuper 是指向当前对象的父类,而 this 是指向当前对象自己。this 常用来区别成员变量和局部变量,比如下面这段代码,我加了个有参构造方法。

public class Parent {

    int a;

    /**
     * 构造方法
     */
    public Parent() {

        System.out.println("这是父类 Parent 的构造方法");
    }

    public Parent(int a) {
        this.a = a;
    }


}

多态

说完继承,我们再来聊聊多态!

多态字面上解释,就是程序可以有多个运行状态。

既然是运行状态,那其实更多的是强调方法的使用。

重载与覆写

方法在两种情况下使用会比较特别,一种是 overload(重载),overload 方法是本类内的新方法,方法名一样,但是参数的类型或数量不同。这种方法没有特殊的标识,通过类内方法是否重名判定。

另外一种就是 override(覆写),override 方法是继承关系下子类的新方法,方法签名和父类完全相同。这种方法都会有 @Override 注解的标识。

package cn.java4u.oo.polymorphism;

/**
 * 动物
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class Animal {


    /**
     * 与 eat(String food) 重载
     */
    public void eat() {
        System.out.println("Animal.eat");
    }

    /**
     * 与 eat() 重载
     *
     * @param food 食物
     */
    public void eat(String food) {
        System.out.println("Animal.eat: " + food);
    }

    /**
     * 覆写
     *
     * @return 字符串
     * @see java.lang.Object#toString
     */
    @Override
    public String toString() {
        return "Animal " + super.toString();
    }
}

举个例子,Animal 类里两个 eat 方法就互为重载方法,toString 方法就是相对于父类方法 java.lang.Object#toString 的覆写方法。

多态就发生在覆写这种场景下。针对某个类型的方法调用,它真正执行的方法取决于运行时期实际类型的方法。比如下面这段代码,当声明类型为 Object ,初始化类型为 Animal 时,你觉得输出的是 AnimaltoString 方法,还是 ObjecttoString 方法?

package cn.java4u.oo.polymorphism;

/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class PolymorphismTest {

    /**
     * 打印对象
     *
     * @param scene 打印场景
     * @param obj   obj
     */
    public static void printObjectString(String scene, Object obj) {

        System.out.println(scene + ": " + obj.toString());

    }

    public static void main(String[] args) {

        // 父类引用初始化父类对象并打印
        Object rootObj = new Object();
        printObjectString("父类引用初始化父类对象", rootObj);

        // 子类引用初始化子类对象并打印
        Animal animal = new Animal();
        printObjectString("子类引用初始化子类对象", animal);


        // 父类引用初始化子类对象并打印
        Object animalWhenParentRef = new Animal();
        printObjectString("父类引用初始化子类对象", animal);
        
    }
}

答案是子类 AnimaltoString 方法!

父类引用初始化父类对象: java.lang.Object@60e53b93
子类引用初始化子类对象: Animal cn.java4u.oo.polymorphism.Animal@5e2de80c
父类引用初始化子类对象: Animal cn.java4u.oo.polymorphism.Animal@5e2de80c

实际类型为 Animal 引用类型为 Object ,调用 toString 方法时,实际上是子类的。因此我们可以得出结论:Java 的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。这种特性就是多态

你会发现 printObjectString 方法的第二个参数,即便声明的是 Object ,实际运行的时候,却可以是它的子类覆写方法。

至此,我们也理出了 Java 实现多态三要素,那就是 继承覆写向上转型。即两个类之间有继承关系,某个类覆写了父类的某个方法,方法的引用会指向子类的实现处。

总结

本文从 Java 的视角出发,分析了 Java 的语言特点,并和 C++ 进行了比较。针对这门典型的面向对象语言,我们又分析了面向对象的概念和思想。接着基于面向对象的特征:抽象、封装、继承和多态,我们又详细的分析了在 Java 中的体现方式,并伴有很多样例代码辅助学习。看完这篇文章,想必你对面向对象这个东西会有更全面的了解。

好啦,本期的分享就到这里,如果各位喜欢我的分享,请务必三连,点赞在看收藏,关注我,这会对我有非常大的帮助。

我们下期再见。

推荐阅读:

1120页的Leetcode算法刷题笔记,完整版PDF开放下载!

《Java 工程师成神之路》.pdf

更多内容请收藏:Java for You

❌
❌