toBeBetterJavaer Documentation

一份通俗易懂、风趣幽默的Java学习指南,内容涵盖Java基础、Java并发编程、Java虚拟机、Java企业级开发、Java面试等核心知识点。学Java,就认准二哥的Java进阶之路😄

17,401 stars Java Java 1 file · ~27,039 tokens #java#jvm#mysql#redis#springboot
RAW Doc

File: README.md

为什么会有这个开源知识库

知识库取名 toBeBetterJavaer,即 To Be Better Javaer,意为「成为一名更好的 Java 程序员」,是我自学 Java 以来所有原创文章和学习资料的大聚合。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。学 Java,就认准 二哥的Java进阶之路😄。

知识库旨在为学习 Java 的小伙伴提供一系列:

  • 优质的原创 Java 教程
  • 全面清晰的 Java 学习路线
  • 免费但靠谱的 Java 学习资料
  • 精选的 Java 岗求职面试指南
  • Java 企业级开发所需的必备技术

赠人玫瑰手有余香。知识库会持续保持更新,欢迎收藏品鉴!

转载须知 :以下所有文章如非文首说明为转载皆为我(沉默王二)的原创,且不允许转载,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境!

推荐你通过在线阅读网站进行阅读,体验更好,速度更快!

如果你更喜欢离线的 PDF 版本,戳这个链接获取👍二哥的 Java 进阶之路.pdf

知识库地图

知识库收录的核心内容就全在这里面了,大类分为 Java 核心、Java 企业级开发、数据库、计算机基础、求职面试、学习资源、程序人生,几乎你需要的这里都有。

一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 13000 多名 球友加入了(马上涨价到 179 元,抓紧时间趁没涨价前加入吧),如果你也需要一个优质的学习环境,扫描下方的优惠券加入我们吧。

新人可免费体验 3 天,不满意可全额退款(只能帮你到这里了😄)。

这是一个 简历精修 + AI/Agent实战项目 + Java 面试指南 + LeetCode 刷题的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的内容,相信能帮助你走的更快、更稳、更远。

学习路线

除了 Java 学习路线,还有 MySQL、Redis、C语言、C++、Python、Go 语言、操作系统、前端、数据结构与算法、蓝桥杯、大数据、Android、.NET等硬核学习路线,欢迎收藏品鉴!

面渣逆袭

面试前必读系列!包括 Java 基础、Java 集合框架、Java 并发编程、Java 虚拟机、Spring、Redis、MyBatis、MySQL、操作系统、计算机网络、RocketMQ、分布式、微服务、设计模式、Linux 等等。

Java基础

Java基础非常重要!包括基础语法、面向对象、集合框架、异常处理、Java IO、网络编程、NIO、并发编程和 JVM。

Java概述及环境配置

Java基础语法

数组&字符串

Java面向对象编程

集合框架(容器)

Java IO

异常处理

常用工具类

Java新特性

Java网络编程

Java NIO

重要知识点

Java并发编程

Java虚拟机

Java进阶

  • 到底能不能成为一名合格的 Java 程序员,从理论走向实战?Java进阶这部分内容就是一个分水岭!
  • 纸上得来终觉浅,须知此事要躬行。

开发/构建工具

工欲善其事必先利其器,这句话大家都耳熟能详了,熟练使用开发/构建工具可以让我们极大提升开发效率,解放生产力。

IDEA

集成开发环境,Java 党主要就是 Intellij IDEA 了,号称史上最强大的 Java 开发工具,没有之一。

Maven

Maven 是目前比较流行的一个项目构建工具,基于 pom 坐标来帮助我们管理第三方依赖,以及项目打包。

Git

Git 是一个分布式版本控制系统,缔造者是大名鼎鼎的林纳斯·托瓦茲 (Linus Torvalds),Git 最初的目的是为了能更好的管理 Linux 内核源码。如今,Git 已经成为全球软件开发者的标配。如果说 Linux 项目促成了开源软件的成功并改写了软件行业的格局,那么 Git 则是改变了全世界开发者的工作方式和写作方式。

Spring

SpringBoot

Netty

辅助工具

开源轮子

分布式

消息队列

数据库

  • 简而言之,就是按照数据结构来组织、存储和管理数据的仓库。几乎所有的 Java 后端开发都要学习数据库这块的知识,包括关系型数据库 MySQL,缓存中间件 Redis,非关系型数据库 MongoDB 等。

MySQL

Redis

MongoDB

计算机基础

  • 计算机基础包括操作系统、计算机网络、计算机组成原理、数据结构与算法等。对于任何一名想要走得更远的 Java 后端开发来说,都是必须要花时间和精力去夯实的。
  • 万丈高露平地起,勿在浮沙筑高台。

求职面试

  • 学习了那么多 Java 知识,耗费了无数的脑细胞,熬掉了无数根秀发,为的是什么?当然是谋取一份心仪的 offer 了。那八股文、面试题、城市选择、优质面经又怎能少得了呢?
  • 千淘万漉虽辛苦,吹尽狂沙始到金。

面试题&八股文

优质面经

面试准备

城市选择

学习资源

  • 不知道学什么?不知道该怎么学?找不到优质的学习资源?这些问题在这里统统都可以找到答案。
  • 我会把自己十多年的编程经验和学习资源毫不保留的分享出来。

PDF下载

学习建议

知识库搭建

从购买阿里云服务器+域名购买+域名备案+HTTP 升级到 HTTPS,全方面记录《二哥的Java进阶之路》知识库的诞生和改进过程,涉及到 docsify、Git、Linux 命令、GitHub 仓库等实用知识点。

本知识库使用 VuePress 搭建,并基于VuePress Theme Hope主题,你可以把仓库拉到本地后直接通过 pnpm docs:clean-dev 跑起来。

前提是你已经安装好 node.js 和 pnpm 环境。

点击链接就可以在本地看到运行后的效果了。

如果想部署服务器,可以进入 scripts 目录,执行 ./deploy-docs.sh 脚本,需要先 cp .env.example .env,把你的服务器地址、用户名、密钥、远程路径等信息填进去。

联系作者

  • 作者是一名普通普通普通普通三连的 Java 后端开发者,热爱学习,热爱分享
  • 参加工作以后越来越理解交流和分享的重要性,在不停地汲取营养的同时,也希望帮助到更多的小伙伴们
  • 二哥的Java进阶之路,不仅是作者自学 Java 以来所有的原创文章和学习资料的大聚合,更是作者向这个世界传播知识的一个窗口。

心路历程

联系方式

原创公众号

GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

star趋势图

友情链接

  • paicoding,⭐️一款好用又强大的开源社区,附详细教程,包括Java、Spring、MySQL、Redis、微服务&分布式、消息队列、操作系统、计算机网络、数据结构与算法等计算机专业核心知识点。学编程,就上技术派😁。
  • Hippo4J,🔥 强大的动态线程池,附带监控报警功能(没有依赖中间件),完全遵循阿里巴巴编码规范。
  • JavaGuide,「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide!

捐赠鼓励

开源不易,如果《二哥的Java进阶之路》对你有些帮助,可以请作者喝杯咖啡,算是对开源做出的一点点鼓励吧!

:gift_heart: 感谢大家对我资金的赞赏,每隔一个月会统计一次。

时间 小伙伴 赞赏金额
2025-07-02 橘子 4元
2025-06-28 m*u 10元
2025-06-15 l*y 5元
2025-05-28 *航 6元
2025-05-25 *星 10元
2025-05-25 *( 6.66元
2025-05-17 *鋈 4元
2025-05-10 *庆 1元
2025-05-08 芋*3 10元
2025-04-17 *南 10元
2025-03-31 :*D 4元
2025-03-26 A*. 6.66元
2025-02-18 R*. 6.66元
2025-02-08 *金 5元
2025-01-17 *蓝 8.88元
2024-12-30 *甜 2元
2024-12-26 *阳 1元
2024-12-18 *。 1.5元
2024-12-06 E*g 5元
2024-12-04 *佚 0.88元
2024-12-02 A*g 6.66元
2024-11-30 1*0 10元
2024-11-23 W*Z 11元
2024-11-17 *旺 2元
2024-11-16 *年 1元
2024-11-14 *🤖 10元
2024-11-13 *光 0.1元
2024-10-25 *陈 1元
2024-10-06 *天 10元
2024-10-04 2*2 20元
2024-09-25 c*l 1元
2024-09-14 .*6 1.9元
2024-08-16 *了 20元
2024-08-14 *李 0.66元
2024-08-12 *Z 6.66元
2024-08-09 *峰 2元
2024-07-13 *运 20元
2024-07-01 *风 1元
2024-06-30 *迷 1元
2024-06-23 *瓦 1元
2024-06-17 *芒 5元
2024-06-13 *啊 9.99元
2024-06-03 S*d 1元
2024-05-23 *气 3元
2024-05-22 w*r 6.6元
2024-05-01 *笑 0.01元
2024-04-24 1*0 3元
2024-04-10 迷*x 21元
2024-04-08 *青 5元
2024-04-08 敲不出来的一个符号 1元
2024-04-07 *i 0.01元
2024-04-06 *牛 10元
2024-04-03 Y*T 10元
2024-04-02 B*E 2元
2024-03-20 *卡 1元
2024-03-18 *嘎 6.66元
2024-03-17 *兴 0.01元
2024-03-12 *鹏 0.02元
2024-03-12 y*u 0.01元
2024-02-29 r*y 6元
2024-02-23 *~ 9.99元
2024-02-21 从头再来 5元
2024-02-15 *斗 10元
2024-02-02 *切 2元
2024-02-01 *康 9元
2024-01-31 *康 1元
2024-01-22 *妙 10元
2024-01-17 *清 9.9元
2024-01-12 *奥 5元
2024-01-04 *👈🏻 1元
2024-01-03 * 3元
2024-01-03 Y*o 2元
2023-12-22 *逗 50元
2023-11-25 *君 2元
2023-10-23 *🐻 6.66元
2023-10-17 *哈 5元
2023-10-12 0*7 7.77元
2023-10-03 S*d 0.5元
2023-09-27 *1 1元
2023-09-25 L*e 10.24元
2023-09-19 *人 2元
2023-09-15 L*D 2元
2023-09-15 *暖 5元
2023-09-11 A*B 1元
2023-08-21 *氏 2元
2023-08-18 *寻 1元
2023-08-03 *案 10.24元
2023-08-02 *, 1元
2023-07-24 m*l 3元
2023-07-20 lzy 6元
2023-07-14 s*! 2元
2023-07-02 *晴 1元
2023-06-26 *雨 6.66元
2023-06-21 *航 6元
2023-06-21 *狼 3元
2023-06-19 *定 2元
2023-06-18 *道 5元
2023-06-16 * 文 1元
2023-06-14 G*e 66.6元
2023-06-07 *. 0.5元
2023-05-23 *W 5元
2023-05-19 *飞 6元
2023-05-10 c*r 1元
2023-04-26 r*J 10.24元
2023-04-22 *明 1元
2023-04-09 * 刀 10元
2023-04-03 *意 0.02元
2023--03-17 *昌 8 元
2023-03-16 * 66.6 元
2023-03-15 *枫 6.6 元
2023-03-10 十年 1 元
2023-03-04 *风 5 元
2023-02-26 一个表情(emoji) 1 元
2023-02-23 曹*n 5元
2023-02-11 昵称加载中. 6.6元
2023-02-09 *明 10元
2023-02-09 *风 5元
2023-02-09 *z 3元
2023-02-09 *夫 10元
2023-02-08 *宝 5 元
2023-01-18 *念 0.01元
2023-01-18 *来 1元
2023-01-10 At 1元
2023-01-07 *忠 5元
2023-12-02 g*g 0.1元
2022-11-13 *王 5元
2022-11-10 *车 1元
2022-11-10 F*k 1元
2022-11-05 *H 3元
2022-11-04 *金 0.02元
2022-11-04 *尘 15元
2022-11-02 *峰 1元
2022-10-29 * 6元
2022-10-28 k*k 1元
2022-10-20 *电 2元
2022-10-15 *深 5元
2022-09-30 *君 1元
2022-09-28 *懂 1元
2022-09-27 *府 1元
2022-09-23 *问号(emogji) 5元
2022-09-23 H*n 1元
2022-09-23 *a 0.01元
2022-09-08 *👀 20元
2022-09-07 丹*1 20元
2022-08-27 *夹 40元
2022-07-06 体*P 2元
2022-07-05 *谦 5元
2022-06-18 *杰 2元
2022-06-15 L*c 15元
2022-06-10 *❤ 1元
2022-06-09 '*' 1元
2022-06-07 *勇 1元
2022-06-03 *鸭 1元
2022-05-12 *烟 10元
2022-04-25 *思 5元
2022-04-20 w*n 1元
2022-04-12 E*e 10 元
2022-03-19 *风 9.9元
2022-03-04 袁晓波 99元
2022-02-17 *色 1元
2022-02-17 M*y 1元
2022-01-28 G*R 6.6元
2022-01-20 *光 50元
2022-01-14 *浩 1元
2022-01-01 刚*好 3.6元
2022-01-01 马*谊 6.6元
2021-12-20 t*1 5 元
2021-10-26 *猫 28 元
2021-10-11 *人 28 元
2021-09-28 *人 1 元
2021-09-05 N*a 3 元
2021-09-02 S*n 6.6 元
2021-08-21 z*s 3 元
2021-08-20 A*g 10 元
2021-08-09 *滚 0.1 元
2021-08-02 *秒 1 元
2021-06-13 *7 28 元
2021-05-04 *学 169 元
2021-04-29 p*e 2 元
2021-04-28 追风筝的神 1 元

参与贡献

  1. 如果你对本项目有任何建议或发现文中内容有误的,欢迎提交 issues 进行指正。
  2. 对于文中我没有涉及到知识点,欢迎提交 PR。

File: docs/src/basic-grammar/basic-data-type.md


title: 教妹学Java:10 分钟掌握Java基本数据类型
shortTitle: Java数据类型
category:

  • Java核心
    tag:
  • Java语法基础
    head:
    • meta
    • name: keywords
      content: Java, 数据类型, 比特, 字节, 基本数据类型, 引用数据类型, 单精度, 双精度, int, char, 包装器类型, 堆, 栈, 内存模型, 类型转换

“Java 是一种静态类型的编程语言,这意味着所有变量必须在使用之前声明好,也就是必须得先指定变量的类型和名称。”我吸了一口麦香可可奶茶后对三妹说。

Java 中的数据类型可分为 2 种:

1)基本数据类型。

基本数据类型是 Java 语言操作数据的基础,包括 boolean、char、byte、short、int、long、float 和 double,共 8 种。

2)引用数据类型。

除了基本数据类型以外的类型,都是所谓的引用类型。常见的有数组(对,没错,数组是引用类型,后面我们会讲)、class(也就是类),以及接口(指向的是实现接口的类的对象)。

来个思维导图,感受下。

变量可以分为局部变量、成员变量、静态变量。

当变量是局部变量的时候,必须得先初始化,否则编译器不允许你使用它。拿 int 来举例吧,看下图。

当变量是成员变量或者静态变量时,可以不进行初始化,它们会有一个默认值,仍然以 int 为例,来看代码:

java
/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class LocalVar {
    private int a;
    static int b;

    public static void main(String[] args) {
        LocalVar lv = new LocalVar();
        System.out.println(lv.a);
        System.out.println(b);
    }
}

来看输出结果:

text
0
0

瞧见没,int 作为成员变量时或者静态变量时的默认值是 0。那不同的基本数据类型,是有不同的默认值和占用大小的,来个表格感受下。

数据类型 默认值 大小
boolean false 不确定
char '\u0000' 2 字节
byte 0 1 字节
short 0 2 字节
int 0 4 字节
long 0L 8 字节
float 0.0f 4 字节
double 0.0 8 字节

01、比特和字节

那三妹可能要问,“比特和字节是什么鬼?”

比特币(Bitcoin)听说过吧?字节跳动(Byte Dance)听说过吧?这些名字当然不是乱起的,确实和比特、字节有关系。

**1)bit(比特)**

比特作为信息技术的最基本存储单位,非常小,但大名鼎鼎的比特币就是以此命名的,它的简写为小写字母“b”。

大家都知道,计算机是以二进制存储数据的,二进制的一位,就是 1 比特,也就是说,比特要么为 0 要么为 1。

**2)Byte(字节)**

通常来说,一个英文字符是一个字节,一个中文字符是两个字节。字节与比特的换算关系是:1 字节 = 8 比特。

在往上的单位就是 KB,并不是 1000 字节,因为计算机只认识二进制,因此是 2 的 10 次方,也就是 1024 个字节。

(终于知道 1024 和程序员的关系了吧?狗头保命)

02、基本数据类型

接下来,我们再来详细地了解一下 8 种基本数据类型。

1)布尔

布尔(boolean)仅用于存储两个值:true 和 false,也就是真和假,通常用于条件的判断。代码示例:

java
boolean hasMoney = true;
boolean hasGirlFriend = false;

根据 Java 语言规范,boolean 类型只有两个值 true 和 false,但在语言层面,Java 没有明确规定 boolean 类型的大小。

那经过我的调查,发现有两种论调。

我们先来看论调一。

对于单独使用的 boolean 类型,JVM 并没有提供专用的字节码指令,而是使用 int 相关的指令 istore 来处理,那么 int 明确是 4 个字节,所以此时的 boolean 也占用 4 个字节。

对于作为数组来使用的 boolean 类型,JVM 会按照 byte 的指令来处理(bastore),那么已知 byte 类型占用 1 个字节,所以此时的 boolean 也占用 1 个字节。

论调二,布尔具体占用的大小是不确定的,取决于 JVM 的具体实现。

boolean: The boolean data type has only two possible values: true and false. Use this data type for simple flags that track true/false conditions. This data type represents one bit of information, but its "size" isn't something that's precisely defined.

可以通过 JOL 工具打印出对象的内存布局,展示 boolean 单独使用和作为数组使用时在内存中的实际占用大小。

java
public class BooleanSizeExample {
    public static void main(String[] args) {
        boolean singleBoolean = true;
        boolean[] booleanArray = new boolean[10];
        
        // 分析内存占用,可以使用第三方工具如 JOL(Java Object Layout)
        System.out.println("Size of single boolean: " + org.openjdk.jol.info.ClassLayout.parseInstance(singleBoolean).toPrintable());
        System.out.println("Size of boolean array: " + org.openjdk.jol.info.ClassLayout.parseInstance(booleanArray).toPrintable());
    }
}

运行结果如下(64 操作系统 JDK 8):

text
Size of single boolean: java.lang.Boolean object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           dd 20 00 f8 (11011101 00100000 00000000 11111000) (-134209315)
     12     1   boolean Boolean.value                             true
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

Size of boolean array: [Z object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           05 00 00 f8 (00000101 00000000 00000000 11111000) (-134217723)
     12     4           (object header)                           0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
     16    10   boolean [Z.<elements>                             N/A
     26     6           (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total

对于单个 boolean 变量来说:

①、对象头(Object Header) 占用了 12 个字节:

  • OFFSET 0 - 4:对象头的一部分,包含对象的标记字段(Mark Word),用于存储对象的哈希码、GC 状态等。
  • OFFSET 4 - 8:对象头的另一部分,通常是指向类元数据的指针(Class Pointer)。
  • OFFSET 8 - 12:对象头的最后一部分,包含锁状态或其他信息。

②、实际的 boolean 值占用 1 个字节,也就是OFFSET 12 - 13。

③、为了满足 8 字节的对齐要求(HotSpot JVM 默认的对象对齐方式),有 3 个字节的填充。OFFSET 13 - 16。

也就是说,尽管 boolean 值本身只需要 1 个字节,但由于对象头和对齐要求,一个 boolean 在内存中占用 16 字节。

对于 boolean 数组来说:

①、对象头(Object Header) 占用了 12 个字节:

  • OFFSET 0 - 4:对象头的一部分,包含对象的标记字段(Mark Word)。
  • OFFSET 4 - 8:对象头的另一部分,包含指向类元数据的指针(Class Pointer)。
  • OFFSET 8 - 12:对象头的最后一部分,通常包含数组的长度信息。

②、数组长度 占用了 4 个字节,此处是 10,OFFSET 12 - 16。

③、实际的 boolean 数组元素,每个 boolean 值占用 1 个字节,总共 10 个字节,OFFSET 16 - 26。

④、为了满足 8 字节对齐要求,有 6 个字节的填充,OFFSET 26 - 32。

也就是说,每个 boolean 数组元素占用 1 个字节,加上对象头、对齐填充和数组长度,包含 10 个元素的 boolean 数组占用 32 字节。

2)byte

一个字节可以表示 2^8 = 256 个不同的值。由于 byte 是有符号的,它的值可以是负数或正数,其取值范围是 -128 到 127(包括 -128 和 127)。

在网络传输、大文件读写时,为了节省空间,常用字节来作为数据的传输方式。代码示例:

java
byte b; // 声明一个 byte 类型变量
b = 10; // 将值 10 赋给变量 b
byte c = -100; // 声明并初始化一个 byte 类型变量 c,赋值为 -100

3)short

short 的取值范围在 -32,768 和 32,767 之间,包含 32,767。代码示例:

java
short s; // 声明一个 short 类型变量
s = 1000; // 将值 1000 赋给变量 s
short t = -2000; // 声明并初始化一个 short 类型变量 t,赋值为 -2000

实际开发中,short 比较少用,整型用 int 就 OK。

3)int

int 的取值范围在 -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之间。如果没有特殊需求,整型数据就用 int。代码示例:

java
int i; // 声明一个 int 类型变量
i = 1000000; // 将值 1000000 赋给变量 i
int j = -2000000; // 声明并初始化一个 int 类型变量 j,赋值为 -2000000

为什么 32 位的有符号整数的取值范围是从 -2^31 到 2^31 - 1 呢?

这是因为其中一位用于表示符号(正或负),剩下的 31 位用于表示数值,这意味着其范围是 -2,147,483,648(即 -2^31)到 2,147,483,647(即 2^31 - 1)。

在二进制系统中,每个位(bit)可以表示两个状态,通常是 0 和 1。对于 32 位得正二进制数,除去符号位,从右到左的每一位分别代表 2^0, 2^1, 2^2, ..., 2^30,这个二进制数转换为十进制就是 2^0 + 2^1 + 2^2 + ... + 2^30,也就是 2,147,483,647。

5)long

long 的取值范围在 -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之间。如果 int 存储不下,就用 long。代码示例:

java
long l; // 声明一个 long 类型变量
l = 100000000000L; // 将值 100000000000L 赋给变量 l(注意要加上 L 后缀)
long m = -20000000000L; // 声明并初始化一个 long 类型变量 m,赋值为 -20000000000L

为了和 int 作区分,long 型变量在声明的时候,末尾要带上大写的“L”。不用小写的“l”,是因为小写的“l”容易和数字“1”混淆。

6)float

float 是单精度的浮点数(单精度浮点数的有效数字大约为 6 到 7 位),32 位(4 字节),遵循 IEEE 754(二进制浮点数算术标准),取值范围为 1.4E-45 到 3.4E+38。float 不适合用于精确的数值,比如说金额。代码示例:

java
float f; // 声明一个 float 类型变量
f = 3.14159f; // 将值 3.14159f 赋给变量 f(注意要加上 f 后缀)
float g = -2.71828f; // 声明并初始化一个 float 类型变量 g,赋值为 -2.71828f

为了和 double 作区分,float 型变量在声明的时候,末尾要带上小写的“f”。不需要使用大写的“F”,是因为小写的“f”很容易辨别。

7)double

double 是双精度浮点数(双精度浮点数的有效数字大约为 15 到 17 位),占 64 位(8 字节),也遵循 IEEE 754 标准,取值范围大约 ±4.9E-324 到 ±1.7976931348623157E308。double 同样不适合用于精确的数值,比如说金额。

代码示例:

java
double myDouble = 3.141592653589793;

在进行金融计算或需要精确小数计算的场景中,可以使用 BigDecimal 类来避免浮点数舍入误差。BigDecimal 可以表示一个任意大小且精度完全准确的浮点数。

在实际开发中,如果不是特别大的金额(精确到 0.01 元,也就是一分钱),一般建议乘以 100 转成整型进行处理。

8)char

char 用于表示 Unicode 字符,占 16 位(2 字节)的存储空间,取值范围为 0 到 65,535。

代码示例:

java
char letterA = 'A'; // 用英文的单引号包裹住。

注意,字符字面量应该用单引号('')包围,而不是双引号(""),因为双引号表示字符串字面量。

03、单精度和双精度

单精度(single-precision)和双精度(double-precision)是指两种不同精度的浮点数表示方法。

单精度是这样的格式,1 位符号,8 位指数,23 位小数。

单精度浮点数通常占用 32 位(4 字节)存储空间。数值范围大约是 ±1.4E-45 到 ±3.4028235E38,精度大约为 6 到 9 位有效数字。

双精度是这样的格式,1 位符号,11 位指数,52 为小数。

双精度浮点数通常占用 64 位(8 字节)存储空间,数值范围大约是 ±4.9E-324 到 ±1.7976931348623157E308,精度大约为 15 到 17 位有效数字。

计算精度取决于小数位(尾数)。小数位越多,则能表示的数越大,那么计算精度则越高。

一个数由若干位数字组成,其中影响测量精度的数字称作有效数字,也称有效数位。有效数字指科学计算中用以表示一个浮点数精度的那些数字。一般地,指一个用小数形式表示的浮点数中,从第一个非零的数字算起的所有数字。如 1.24 和 0.00124 的有效数字都有 3 位。

以下是确定有效数字的一些基本规则:

  • 非零数字总是有效的。
  • 位于两个非零数字之间的零是有效的。
  • 对于小数,从左侧开始的第一个非零数字之前的零是无效的。
  • 对于整数,从右侧开始的第一个非零数字之后的零是无效的。

下面是一些示例,说明如何确定有效数字:

  • 1234:4 个有效数字(所有数字都是非零数字)
  • 1002:4 个有效数字(零位于两个非零数字之间)
  • 0.00234:3 个有效数字(从左侧开始的前两个零是无效的)
  • 1200:2 个有效数字(从右侧开始的两个零是无效的)

04、int 和 char 类型互转

int 和 char 之间比较特殊,可以互转,也会在以后的学习当中经常遇到。

1)可以通过强制类型转换将整型 int 转换为字符 char。

java
int value_int = 65;
char value_char  = (char) value_int;
System.out.println(value_char);

输出 A(其 ASCII 值可以通过整数 65 来表示)。

2)可以使用 Character.forDigit() 方法将整型 int 转换为字符 char,参数 radix 为基数,十进制为 10,十六进制为 16。。

java
int radix = 10;
int value_int = 6;
char value_char = Character.forDigit(value_int , radix);
System.out.println(value_char );

Character 为 char 的包装器类型。我们随后会讲。

3)可以使用 int 的包装器类型 Integer 的 toString() 方法+String 的 charAt() 方法转成 char。

java
int value_int = 1;
char value_char = Integer.toString(value_int).charAt(0);
System.out.println(value_char );

4)char 转 int

当然了,如果只是简单的 char 转 int,直接赋值就可以了。

java
int a = 'a';

因为发生了自动类型转换,后面会细讲。

不过,如果字符本身就是数字,这种方法就行不通了。

java
int a = '1';

这样的话,a 的值是 49,而不是 1。因为字符 '1' 的 ASCII 码是 49。

那么,怎么才能把字符 '1' 转成数字 1 呢?

可以使用 Character.getNumericValue() 方法。

java
int a = Character.getNumericValue('1');

这样的话,a 的值就是 1 了。

除此之外,还可以使用 Character.digit() 方法。

java
int a = Character.digit('1', 10);

这样的话,a 的值也是 1。

因为这两个方法的内部实现都大差不差,大家可以研究一下源码。

那还有一种更直观的方法,就是 - '0' 方法。

java
int a = '1' - '0';

这样的话,a 的值也是 1。这是因为在 ASCII 编码和 Unicode 编码(Java 使用 Unicode 编码)中,数字字符 '0' 到 '9' 是连续排列的,并且它们的编码值是顺序递增的。

字符 '0' 的编码值是 48,字符 '1' 的编码值是 49,依此类推,字符 '9' 的编码值是 57。

当从一个字符的编码值中减去字符 '0' 的编码值(即 48),结果就是该字符所表示的数字值。例如,对于字符 '5',其编码值是 53。计算 53 - 48 得到 5,这就是字符 '5' 所表示的数字值。

05、包装器类型

包装器类型(Wrapper Types)是 Java 中的一种特殊类型,用于将基本数据类型(如 int、float、char 等)转换为对应的对象类型。

Java 提供了以下包装器类型,与基本数据类型一一对应:

  • Byte(对应 byte)
  • Short(对应 short)
  • Integer(对应 int)
  • Long(对应 long)
  • Float(对应 float)
  • Double(对应 double)
  • Character(对应 char)
  • Boolean(对应 boolean)

包装器类型允许我们使用基本数据类型提供的各种实用方法,并兼容需要对象类型的场景。例如,我们可以使用 Integer 类的 parseInt 方法将字符串转换为整数,或使用 Character 类的 isDigit 方法检查字符是否为数字,还有前面提到的 Character.forDigit() 方法。

下面是一个简单的示例,演示了如何使用包装器类型:

java
// 使用 Integer 包装器类型
Integer integerValue = new Integer(42);
System.out.println("整数值: " + integerValue);

// 将字符串转换为整数
String numberString = "123";
int parsedNumber = Integer.parseInt(numberString);
System.out.println("整数值: " + parsedNumber);

// 使用 Character 包装器类型
Character charValue = new Character('A');
System.out.println("字符: " + charValue);

// 检查字符是否为数字
char testChar = '9';
if (Character.isDigit(testChar)) {
System.out.println("字符是个数字.");
}

上面的示例中,我们创建了一个 Integer 类型的对象 integerValue 并为其赋值 42。然后,我们将其值打印到控制台。

我们有一个包含数字的字符串 numberString。我们使用 Integer.parseInt() 方法将其转换为整数 parsedNumber。然后,我们将转换后的值打印到控制台。

比如说 parseInt() 用于将字符串转换为整数,这也是非常常用的一个方法,尤其是遇到“数字字符串”转整数的时候。

java
String text = "123";
int number = Integer.parseInt(text);
System.out.println(number);

可以简单看一下 parseInt() 的源码:

java
public static int parseInt(String s, int radix) throws NumberFormatException {
    // 如果字符串为空或基数不在有效范围内,抛出 NumberFormatException
    if (s == null || radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) {
        throw new NumberFormatException();
    }

    int result = 0; // 用于存储解析结果的变量
    boolean negative = false; // 标记数字是否为负数
    int i = 0, len = s.length(); // i 是字符索引,len 是字符串长度
    int limit = -Integer.MAX_VALUE; // 溢出检查的上限

    if (len > 0) {
        char firstChar = s.charAt(0); // 获取字符串的第一个字符
        if (firstChar == '-') { // 如果是负号
            negative = true; // 设置负数标记
            limit = Integer.MIN_VALUE; // 调整溢出上限为 Integer 的最小值
            i++;
        } else if (firstChar == '+') { // 如果是正号
            i++; // 仅跳过,不做额外操作
        }

        int multmin = limit / radix; // 计算溢出检查的临界值
        while (i < len) {
            // 将字符转换为对应的数字值
            int digit = Character.digit(s.charAt(i++), radix);
            if (digit < 0 || result < multmin || result * radix < limit + digit) {
                // 如果字符不是有效数字或者结果溢出,抛出 NumberFormatException
                throw new NumberFormatException();
            }
            // 累积结果
            result = result * radix - digit;
        }
    } else {
        // 如果字符串为空,抛出 NumberFormatException
        throw new NumberFormatException();
    }

    // 根据正负号返回最终结果
    return negative ? result : -result;
}

简单解释一下:

  1. 空值检查:首先检查输入字符串是否为 null,如果是,则抛出 NumberFormatException。

  2. 符号处理:检查字符串的第一个字符以确定数字的符号(正或负)。如果字符串以“-”开头,则数字为负数,以“+”或数字开头则为正数。

  3. 数字转换:遍历字符串中的每个字符,将字符转换为对应的数字。这是通过从字符中减去 '0' 的 ASCII 值来实现的。

  4. 结果计算:计算最终的数字值。这是通过将每个数字乘以其位置权重(10 的幂)并累加到结果中来完成的。

  5. 溢出检查:在整个转换过程中,代码会检查是否有溢出的风险。如果检测到溢出,将抛出 NumberFormatException。

  6. 返回结果:根据数字的符号返回最终结果。

这个源码对以后学习 LeetCode 的第八题「字符串转换整数 (atoi)」非常有帮助,题解我已经放到技术派的《二哥的 LeetCode 刷题笔记》中,可以作为参考。

我们有一个字符变量 testChar,并为其赋值字符 '9'。我们使用 Character.isDigit() 方法检查 testChar 是否为数字字符。如果是数字字符,我们将输出一条消息到控制台。

从 Java 5 开始,自动装箱(Autoboxing)和自动拆箱(Unboxing)机制允许我们在基本数据类型和包装器类型之间自动转换,无需显式地调用构造方法或转换方法(链接里会细讲)。

java
Integer integerValue = 42; // 自动装箱,等同于 new Integer(42)
int primitiveValue = integerValue; // 自动拆箱,等同于 integerValue.intValue()

06、引用数据类型

基本数据类型在作为成员变量和静态变量的时候有默认值,引用数据类型也有的(学完数组&字符串,以及面向对象编程后会更加清楚,这里先简单过一下)。

String 是最典型的引用数据类型,所以我们就拿 String 类举例,看下面这段代码:

java
/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class LocalRef {
    private String a;
    static String b;

    public static void main(String[] args) {
        LocalRef lv = new LocalRef();
        System.out.println(lv.a);
        System.out.println(b);
    }
}

输出结果如下所示:

text
null
null

null 在 Java 中是一个很神奇的存在,在你以后的程序生涯中,见它的次数不会少,尤其是伴随着令人烦恼的“空指针异常”,也就是所谓的 NullPointerException。

也就是说,引用数据类型的默认值为 null,包括数组和接口。

那你是不是很好奇,为什么数组和接口也是引用数据类型啊?

先来看数组:

java
int [] arrays = {1,2,3};
System.out.println(arrays);

arrays 是一个 int 类型的数组,对吧?打印结果如下所示:

text
[I@2d209079

[I 表示数组是 int 类型的,@ 后面是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 java.lang.Object 类的 toString() 方法就明白了。

数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢?

一个合理的解释是 Java 将其隐藏了。假如真的存在一个 Array.java,我们也可以假想它真实的样子,它必须要定义一个容器来存放数组的元素,就像 String 类那样。

java
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

数组内部定义数组?没必要的!

再来看接口:

java
List<String> list = new ArrayList<>();
System.out.println(list);

List 是一个非常典型的接口:

java
public interface List<E> extends Collection<E> {}

而 ArrayList 是 List 接口的一个实现:

java
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}

对于接口类型的引用变量来说,你没法直接 new 一个:

只能 new 一个实现它的类的对象——那自然接口也是引用数据类型了。

来看一下基本数据类型和引用数据类型之间最大的差别。

基本数据类型:

  • 1、变量名指向具体的数值。
  • 2、基本数据类型存储在栈上。

引用数据类型:

  • 1、变量名指向的是存储对象的内存地址,在栈上。
  • 2、内存地址指向的对象存储在堆上。

07、堆和栈

看到这,三妹是不是又要问,“堆是什么,栈又是什么?”

堆是堆(heap),栈是栈(stack),如果看到“堆栈”的话,请不要怀疑自己,那是翻译的错,堆栈也是栈,反正我很不喜欢“堆栈”这种叫法,容易让新人掉坑里。

堆是在程序运行时在内存中申请的空间(可理解为动态的过程);切记,不是在编译时;因此,Java 中的对象就放在这里,这样做的好处就是:

当需要一个对象时,只需要通过 new 关键字写一行代码即可,当执行这行代码时,会自动在内存的“堆”区分配空间——这样就很灵活。

栈,能够和处理器(CPU,也就是脑子)直接关联,因此访问速度更快。既然访问速度快,要好好利用啊!Java 就把对象的引用放在栈里。为什么呢?因为引用的使用频率高吗?

不是的,因为 Java 在编译程序时,必须明确的知道存储在栈里的东西的生命周期,否则就没法释放旧的内存来开辟新的内存空间存放引用——空间就那么大,前浪要把后浪拍死在沙滩上啊。

这么说就理解了吧?

如果还不理解的话,可以看一下这个视频,讲的非常不错:什么是堆?什么是栈?他们之间有什么区别和联系?

用图来表示一下,左侧是栈,右侧是堆。

这里再补充一些额外的知识点,能看懂就继续吸收,看不懂可以先去学下一节,以后再来补,没关系的。学习就是这样,可以跳过,可以温故。

举个例子。

java
String a = new String("沉默王二")

这段代码会先在堆里创建一个 沉默王二的字符串对象,然后再把对象的引用 a 放到栈里面。这里面还会涉及到字符串常量池,后面会讲。

那么对于这样一段代码,有基本数据类型的变量,有引用类型的变量,堆和栈都是如何存储他们的呢?

java
public void test()
{
    int i = 4;
    int y = 2;
    Object o1 = new Object();
}

我来画个图表示下。

应该一目了然了吧?

“好了,三妹,关于 Java 中的数据类型就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。

08、小结

本文详细探讨了 Java 数据类型,包括比特与字节、基本数据类型、单精度与双精度、int 与 char 互转、包装器类型、引用数据类型以及堆与栈的内存模型。通过阅读本文,你将全面了解 Java 数据类型的概念与使用方法,为 Java 编程打下坚实基础。


GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。


File: docs/src/basic-grammar/basic-exercise.md


title: Java 语法基础练习题
shortTitle: Java语法基础练习题
category:

  • Java核心
    tag:
  • Java语法基础
    head:
    • meta
    • name: keywords
      content: Java, Java语法基础, 练习题

翻转整数

给定一个 32 位有符号整数,将整数中的数字进行反转。

示例 1:

text
输入: 123
输出: 321

示例 2:

text
输入: -123
输出: -321

如果反转后整数溢出那么就返回 0。

java
public class ReverseInteger {
    public static void main(String[] args) {
        int x = 123;
        int y = -123;
        System.out.println(reverse(x));
        System.out.println(reverse(y));
    }

    public static int reverse(int x) {
        int rev = 0; // 用于存储反转后的结果
		while (x != 0) {
			int pop = x % 10; // 获取 x 的最后一位数字
			x /= 10; // 移除 x 的最后一位数字

			// 检查溢出:如果 rev > Integer.MAX_VALUE/10 或 rev < Integer.MIN_VALUE/10,则会溢出
			if (rev > Integer.MAX_VALUE / 10 || (rev == Integer.MAX_VALUE / 10 && pop > Integer.MAX_VALUE % 10)) return 0;
			if (rev < Integer.MIN_VALUE / 10 || (rev == Integer.MIN_VALUE / 10 && pop < Integer.MIN_VALUE % 10)) return 0;

			rev = rev * 10 + pop; // 将 pop 添加到 rev 的最后
		}
		return rev; // 返回反转后的整数
    }
}

这道题其实是 LeetCode 的第 7 题,如果你想看看更多的解题思路,可以看看这篇文章:LeetCode 7. 整数反转,我放在技术派的《二哥的 LeetCode 刷题笔记》专栏中。

这道题其实很好的考察了 int 的基本数据类型、取余和除法运算符,以及 if 和 while 语句的使用。

字符串转换整数

请你来实现一个 parseInt 方法,使其能将字符串转换成整数。

示例 1(正数):

text
输入: "42"
输出: 42

示例 2(带空格的负数):

text
输入: "   -42"
输出: -42

示例 3(带非数字的字符):

text
输入: "4193 with words"
输出: 4193

示例 4(超出 int 范围):

text
输入: "91283472332"
输出: 2147483647
java
public class StringToInteger {
    public static void main(String[] args) {
        String str1 = "42";
        String str2 = "   -42";
        String str3 = "4193 with words";
        String str4 = "91283472332";
        System.out.println(parseInt(str1));
        System.out.println(parseInt(str2));
        System.out.println(parseInt(str3));
        System.out.println(parseInt(str4));
    }

    public static int parseInt(String str) {
        int index = 0; // 用于遍历字符串
        int sign = 1; // 用于标记正负号
        int total = 0; // 用于存储转换后的整数

        // 1. 跳过前面的空格
        while (index < str.length() && str.charAt(index) == ' ') index++;

        // 2. 检查正负号
        if (index < str.length() && (str.charAt(index) == '+' || str.charAt(index) == '-')) {
            sign = str.charAt(index) == '+' ? 1 : -1;
            index++;
        }

        // 3. 转换数字
        while (index < str.length()) {
            int digit = str.charAt(index) - '0'; // 获取当前字符对应的数字
            if (digit < 0 || digit > 9) break; // 如果不是数字则退出循环
            // 检查溢出:如果 total > Integer.MAX_VALUE/10 或 total == Integer.MAX_VALUE/10 且 digit > Integer.MAX_VALUE%10,则会溢出
            if (total > Integer.MAX_VALUE / 10 || (total == Integer.MAX_VALUE / 10 && digit > Integer.MAX_VALUE % 10)) {
                return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            }
            total = total * 10 + digit; // 将 digit 添加到 total 的最后
            index++;
        }
        return total * sign; // 返回转换后的整数
    }
}

这道题其实是 LeetCode 的第 8 题,如果你想看看更多的解题思路,可以看看这篇文章:LeetCode 8. 字符串转换整数 (atoi),我放在技术派的《二哥的 LeetCode 刷题笔记》专栏中。

这道题其实很好的字符与整数之间的转换,以及 if 和 while 语句的使用。超纲的内容就是字符串的处理,比如说去空格(trim()),比如说取字符(charAt()),但这些我们在后面的章节中都会讲到。

总结

经过这些练习题的练习,我想你就能完全掌握 Java 的语法知识了。


GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。


File: docs/src/basic-grammar/bigdecimal-biginteger.md


title: BigDecimal,非常大的高精度浮点数
shortTitle: 掌握BigDecimal
author: 毅航
category:

  • 微信公众号

BigDecimal是Java中用于浮点数数值计算的类,其主要适合用于处理需要精确表示和运算的场景。「BigDecimal不仅能精确表示非常大的或非常小的数字,同时还提供任意精度的运算。其有效的解决了浮点数(float和double)在进行精确计算时可能出现的舍入误差问题。」

BigDecimal简介

在处理金融、科学等领域的计算时,为了解决double或float在计算值存在的精度缺失问题BigDecimal应运而生。BigDecimal在设计之初**「皆在提供更高的精度和准确性,以确保浮点数运算的准确性」**。因此其具有如下特点:

  1. 「高精度」:BigDecimal能够精确表示非常大的或非常小的数字,并且提供任意精度的运算。
  2. 「不可变性」:BigDecimal对象是不可变的。一旦创建,数值就不会改变。所有的算术运算都会返回一个新的BigDecimal对象,而不会修改原来的对象。这种设计使得BigDecimal是线程安全的。
  3. 「丰富的运算方法」:BigDecimal提供了丰富的算术运算方法,如add(加法)、subtract(减法)、multiply(乘法)和divide(除法),以及用于舍入、取整和比较的方法。
  4. 「灵活的舍入模式」:提供多种舍入模式(如四舍五入、向上取整等),确保结果的精度和舍入行为可控。

总的来看,「BigDecimal通过其对象的不可变性,从而确保了线程安全;与此同时,其还并提供丰富的算术运算方法(如加法、减法、乘法、除法)和多种舍入模式(如四舍五入、向上取整等),从而满足精确数值计算的需求。」

BigDecimal数据存储的秘密

对BigDecimal有了基础认识后,接下来我们便通过Debug的形式来看看BigDecimal内部究竟是如来实现数据的高精度的存储的。为此我们首先通过如下的语句来构建一个BigDecimal对象

java
BigDecimal bigDecimal = new BigDecimal("3.1415926");

运行代码进入Idea的Debug模式后,可以看到如下内容:

不难发现,对于BigDecimal对象而言其内部有 「intVal、scal、precision、stringCache、initCompact等五个重要属性。」   进一步,翻开BigDecimal源码,可以看到这五个属性各自对应的类型:

java
public class BigDecimal extends Number implements Comparable {
     
     private final BigInteger intVal;

     private final int scale;  
                               
     private transient int precision;

     private transient String stringCache;

   
     private final transient long intCompact;

具体来看,「intVal为一个BigInteger对象,其主要用于保存超出基本类型的数值。」 例如:对于Long数据类型来看,其最大类型为0x7fffffffffffffff即9223372036854775807。因此如下的赋值BigDecimal bigDecimal = new BigDecimal("9223372036854775808")其已然超出了Java中基础类型所能表示的范围,而此时在bigDecimal对象中,其内部的intVal如下所示,不难发现9223372036854775808被赋值给intVal。

明白了BigDecimal中intVal属性的存储规则后,再来看其中的scale、precision所标示的含义。「其中scale表示小数点后的位数而precision则代表BigDecimal中数据的总位数,即包括整数和小数部分。」

进一步,「BigDecimal的stringCache属性则主要用于保存BigDecimal数据所转成的字符串信息,而intCompact则用于将long数值以内的数据转为基本数据类型long进行存储。」

(注:如果数据类型范围超过long所能表示的范围,则会将数据保存至intVal中)

此外还要注意一点,如果是包含小数点的数据其会将其小数点去掉,进而保存其去掉小数点后的数据。例如new BigDecimal("3.1415926")在该BigDecimal对象中intCompact = 31415926。

BigDecimal的最佳实践

知晓了BigDecimal内部对于浮点数据的存储原理后,接下来我们来谈一谈有关BigDecimal的几点最佳实践,以避免在使用BigDecimal时踩坑。

❝

1.为了避免精度丢失,尽量使用BigDecimal(String val)构造方法或者  BigDecimal.valueOf(double val)

❞

如果使用double 类型的数据来构建一个 BigDecimal 对象时,其会出现精度丢失的问题。这主要是因为 double 类型本身在表示浮点数时存在精度限制。

具体来看,double 类型使用 IEEE 754标准的双精度浮点数格式,该格式在二进制表示中无法精确地表示所有十进制的小数。例如,十进制数 0.1 在二进制浮点数中是一个无限循环小数,只能近似表示为 0.1000000000000000055511151231257827021181583404541015625。而使用 new BigDecimal(double) 构造函数时double类型的数值的会将其近似值传递给 BigDecimal,进入导致精度丢失。例如:

java
double value = 0.1;
BigDecimal bd = new BigDecimal(value);
System.out.println(bd);

上述代码最终会输出:0.1000000000000000055511151231257827021181583404541015625而我们所期待的 BigDecimal 实际为 0.1。因此为了避免构建BigDecimal时出现精度丢失的问题,「推荐使用它的BigDecimal(String val)构造方法或者  BigDecimal.valueOf(double val) 静态方法来创建对象。」

❝

2.使用 BigDecimal进行除法运算时,指明数据结果的精度

❞

BigDecimal 在进行除法运算时,「如果不指定截取的精度和舍入模式,当出现数据无法整除时,会出现 ArithmeticException 异常」。例如 1 / 3时其会得到一个无限循环小数。这时如果没有明确指定精度和舍入方式,BigDecimal 将无法完成除法运算并抛出异常。

java
public class BigDecimalDivisionExample {
     public static void main(String[] args) {
         BigDecimal num1 = new BigDecimal("1");
         BigDecimal num2 = new BigDecimal("3");
         BigDecimal result = num1.divide(num2);
 }

在上述代码中,num1 / num2的结果为一个无限循环小数 0.333...。「由于我们并未在代码中指定精度和舍入模式,所以当执行上述代码时如出现如下异常」:Exception: Non-terminating decimal expansion; no exact representable decimal result.

为了避免上述异常的发生,可以再执行divide显示的指定精度截取方式。具体方式如下:num1.divide(num2,2, RoundingMode.HALF_UP);在本例中对数据保留了两位小数,同时使用RoundingMode.HALF_UP四舍五入的截取方式。

事实上 BigDecimal除了外RoundingMode.HALF_UP的舍入方式外,还有如下的截取方式:

  • RoundingMode.HALF_UP:四舍五入,向上舍入。
  • RoundingMode.HALF_DOWN:四舍五入,向下舍入。
  • RoundingMode.HALF_EVEN:四舍五入,如果舍弃部分等于 0.5,则舍入到最接近的偶数。

❝

3.根据业务需要,合理的使用compareTo和equals

❞

由于BigDecimal 内部对 equals方法逻辑进行了重写,这使得equals方法不仅比较数值部分,还比较标度。因此只有数值和标度都相同时equals 方法才会返回 true。例如:

java
public class BigDecimalComparison {
     public static void main(String[] args) {
         BigDecimal bd1 = new BigDecimal("1.0");
         BigDecimal bd2 = new BigDecimal("1.00");
         
         System.out.println(bd1.equals(bd2)); // 输出 false
     }
 }

在这个例子中,bd1 = 1.0  bd2 = 1.00 两个数的数值部分代表的含义是完全相同的,但其精度却不同,此时如果使用equals 方法进行比较,则会返回 false。如果贸然使用equals 是很容易导致出现意料之外的结果。

为了保证数值的比较,BigDecimal 内部也对compareTo 方法进行了重写,使得compareTo方法只比较BigDecimal的数值部分而不考虑标度。「因此如果两个 BigDecimal对象的数值相等,即使标度不同compareTo 方法也会认为它们相等。」

java
public class BigDecimalComparison {
     public static void main(String[] args) {
         BigDecimal bd1 = new BigDecimal("1.0");
         BigDecimal bd2 = new BigDecimal("1.00");

         System.out.println(bd1.compareTo(bd2)); // 
     }
 }

在这个例子中最终的输出结果为0,即代表bd1和bd2相等。这主要是因为bd1和bd2的数值相等因此compareTo 方法返回 0。

因此对于为了避免不必要的混淆和错误,尽量遵循以下最佳实践:

  • 「明确比较目的」:在使用 BigDecimal 进行比较时,首先明确你的比较目的是检查数值相等还是完全相等(包括标度)。如果仅比较数值,请使用 compareTo 方法。如果需要完全相等,请使用 equals 方法。
  • 「避免误解」:理解 BigDecimal 的 equals 方法会考虑标度,而 compareTo 方法只比较数值。在常见的数值比较中,更推荐使用 compareTo 方法。

❝

4.慎用BigDecimal的toString方法

❞

BigDecimal内部对toString方法进行重载,这使得BigDecimal 的 toString 方法会自动去除尾随零,并且使用科学计数法表示非常大的或非常小的数值。例如:

java
public class BigDecimalToStringExample {
     public static void main(String[] args) {
         BigDecimal bd1 = new BigDecimal("123.4500");
         BigDecimal bd2 = new BigDecimal("0.00012345");

         System.out.println(bd1.toString()); 
         System.out.println(bd2.toString()); 
     }
 }

上述代码分别会输出123.45、1.2345E-4。其中bd1 的尾随零被去除,而 bd2 使用了科学计数法进行数据的表示。而为了避免这类问题的发生,可以使用 BigDecimal 的 toPlainString 方法。该方法不会去除尾随零,也不会使用科学计数法。

java
import java.math.BigDecimal;

 public class BigDecimalToPlainStringExample {
     public static void main(String[] args) {
         BigDecimal bd1 = new BigDecimal("123.4500");
         BigDecimal bd2 = new BigDecimal("0.00012345");
         

         System.out.println(bd1.toPlainString()); // 输出 123.4500
         System.out.println(bd2.toPlainString()); // 输出 0.00012345

     }
 }

不难看出,在这个例子中toPlainString 方法保留了尾随零,并且没有使用科学计数法,输出格式更加直观。

  • 「toString 方法」:自动去除尾随零,使用科学计数法表示非常大或非常小的数值。可能导致格式不符合预期。
  • 「toPlainString 方法」:不会去除尾随零,不会使用科学计数法,适合需要保留原始数值格式的场景。

总结

本文主要对BigDecimal内部对于浮点数的存储规则进行分析,以加深读者对于BigDecimal的理解。同时整理了如下五条BigDecimal使用的最佳实践:

  1. 为了避免精度丢失,尽量使用BigDecimal(String val)构造方法或者  BigDecimal.valueOf(double val) ;
  2. 使用 BigDecimal进行除法运算时,指明数据结果的精度;
  3. 根据业务需要,合理的使用compareTo和equals;
  4. 慎用BigDecimal的toString方法。

参考链接:https://mp.weixin.qq.com/s/ShXkr9KKXsDBvmh5PlUgUA,整理:沉默王二


File: docs/src/basic-grammar/flow-control.md


title: Java流程控制语句详解:带你轻松学会if switch while for
shortTitle: Java流程控制语句
category:

  • Java核心
    tag:
  • Java语法基础
    head:
    • meta
    • name: keywords
      content: Java, 流程控制语句, if, switch, while, for, 控制结构, 编程基础, 语法结构

“二哥,流程控制语句都有哪些呢?”三妹的脸上泛着甜甜的笑容,她开始对接下来要学习的内容充满期待了,这正是我感到欣慰的地方。

“比如说 if-else、switch、for、while、do-while、return、break、continue 等等,接下来,我们一个个来了解下。”

01、if-else 相关

**1)if 语句**

if 语句的格式如下:

java
if(布尔表达式){  
// 如果条件为 true,则执行这块代码
}

画个流程图表示一下:

来写个示例:

java
int age = 20;
if (age < 30) {
    System.out.println("青春年华");
}

输出:

text
青春年华

**2)if-else 语句**

if-else 语句的格式如下:

java
if(布尔表达式){  
// 条件为 true 时执行的代码块
}else{  
// 条件为 false  时执行的代码块
}

画个流程图表示一下:

来写个示例:

java
int age = 31;
if (age < 30) {
    System.out.println("青春年华");
} else {
    System.out.println("而立之年");
}

输出:

text
而立之年

除了这个例子之外,还有一个判断闰年(被 4 整除但不能被 100 整除或者被 400 整除)的例子:

java
int year = 2020;
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) {
    System.out.println("闰年");
} else {
    System.out.println("普通年份");
}

输出:

text
闰年

如果执行语句比较简单的话,可以使用三元运算符来代替 if-else 语句,如果条件为 true,返回 ? 后面 : 前面的值;如果条件为 false,返回 : 后面的值。

java
int num = 13;
String result = (num % 2 == 0) ? "偶数" : "奇数";
System.out.println(result);

输出:

text
奇数

**3)if-else-if 语句**

if-else-if 语句的格式如下:

java
if(条件1){  
// 条件1 为 true 时执行的代码
}else if(条件2){  
// 条件2 为 true 时执行的代码
}  
else if(条件3){  
// 条件3 为 true 时执行的代码
}  
...  
else{  
// 以上条件均为 false 时执行的代码
}

画个流程图表示一下:

来写个示例:

java
int age = 31;
if (age < 30) {
    System.out.println("青春年华");
} else if (age >= 30 && age < 40 ) {
    System.out.println("而立之年");
} else if (age >= 40 && age < 50 ) {
    System.out.println("不惑之年");
} else {
    System.out.println("知天命");
}

输出:

text
而立之年

**4)if 嵌套语句**

if 嵌套语句的格式如下:

java
if(外侧条件){    
     // 外侧条件为 true 时执行的代码 
    if(内侧条件){  
        // 内侧条件为 true 时执行的代码
    }    
}

画个流程图表示一下:

来写个示例:

java
int age = 20;
boolean isGirl = true;
if (age >= 20) {
    if (isGirl) {
        System.out.println("女生法定结婚年龄");
    }
}

输出:

text
女生法定结婚年龄

02、switch 语句

switch 语句用来判断变量与多个值之间的相等性。变量的类型可以是:

  • byte、short、char、int:基本整数类型。
  • String:字符串类型。
  • 枚举类型:自定义的枚举类型。
  • 包装类:如 Byte、Short、Character、Integer。

来看一下 switch 语句的格式:

java
switch(变量) {    
case 可选值1:    
 // 可选值1匹配后执行的代码;    
 break;  // 该关键字是可选项
case 可选值2:    
 // 可选值2匹配后执行的代码;    
 break;  // 该关键字是可选项
......    
    
default: // 该关键字是可选项     
 // 所有可选值都不匹配后执行的代码 
}
  • 变量可以有 1 个或者 N 个值。
  • 值类型必须和变量类型是一致的,并且值是确定的。
  • 值必须是唯一的,不能重复,否则编译会出错。
  • break 关键字是可选的,如果没有,则执行下一个 case,如果有,则跳出 switch 语句。
  • default 关键字也是可选的。

画个流程图:

来个示例:

java
int age = 20;
switch (age) {
    case 20 :
        System.out.println("上学");
        break;
    case 24 :
        System.out.println("苏州工作");
        break;
    case 30 :
        System.out.println("洛阳工作");
        break;
    default:
        System.out.println("未知");
        break; // 可省略
}

输出:

text
上学

当两个值要执行的代码相同时,可以把要执行的代码写在下一个 case 语句中,而上一个 case 语句中什么也没有,来看一下示例:

java
String name = "沉默王二";
switch (name) {
    case "詹姆斯":
        System.out.println("篮球运动员");
        break;
    case "穆里尼奥":
        System.out.println("足球教练");
        break;
    case "沉默王二":
    case "沉默王三":
        System.out.println("乒乓球爱好者");
        break;
    default:
        throw new IllegalArgumentException(
                "名字没有匹配项");

}

输出:

text
乒乓球爱好者

枚举作为 switch 语句的变量也很常见,来看例子:

java
public class SwitchEnumDemo {
    public enum PlayerTypes {
        TENNIS,
        FOOTBALL,
        BASKETBALL,
        UNKNOWN
    }

    public static void main(String[] args) {
        System.out.println(createPlayer(PlayerTypes.BASKETBALL));
    }

    private static String createPlayer(PlayerTypes playerType) {
        switch (playerType) {
            case TENNIS:
                return "网球运动员费德勒";
            case FOOTBALL:
                return "足球运动员C罗";
            case BASKETBALL:
                return "篮球运动员詹姆斯";
            case UNKNOWN:
                throw new IllegalArgumentException("未知");
            default:
                throw new IllegalArgumentException(
                        "运动员类型: " + playerType);

        }
    }
}

输出:

text
篮球运动员詹姆斯

但 switch 不支持 long、float、double 类型,这是因为:

  • long 是 64 位整数,不在 switch 一开始设计的范围内(32 位的 int 在大多数情况下就够用了)。
  • float 和 double 是浮点数,浮点数的比较不如整数简单和直接,存在精度误差。

03、for 循环

**1)普通 for 循环**

普通的 for 循环可以分为 4 个部分:

1)初始变量:循环开始执行时的初始条件。

2)条件:循环每次执行时要判断的条件,如果为 true,就执行循环体;如果为 false,就跳出循环。当然了,条件是可选的,如果没有条件,则会一直循环。

3)循环体:循环每次要执行的代码块,直到条件变为 false。

4)自增/自减:初始变量变化的方式。

来看一下普通 for 循环的格式:

java
for(初始变量;条件;自增/自减){  
// 循环体
}

画个流程图:

来个示例:

java
for (int i = 0; i < 5; i++) {
    System.out.println("沉默王三好美啊");
}

输出:

text
沉默王三好美啊
沉默王三好美啊
沉默王三好美啊
沉默王三好美啊
沉默王三好美啊

“哎呀,二哥,你真的是变着法夸我啊。”

“非也非也,三妹,你看不出我其实在夸我自己吗?循环语句还可以嵌套呢,这样就可以打印出更好玩的呢,你要不要看看?”

“好呀好呀!”

“看好了啊。”

java
for (int i = 0; i < 5; i++) {
    for (int j = 0;j<= i;j++) {
        System.out.print("❤");
    }
    System.out.println();
}

打印出什么玩意呢?

text
❤
❤❤
❤❤❤
❤❤❤❤
❤❤❤❤❤

“哇,太不可思议了,二哥。”

“嘿嘿。”

**2)for-each**

for-each 循环通常用于遍历数组和集合,它的使用规则比普通的 for 循环还要简单,不需要初始变量,不需要条件,不需要下标来自增或者自减。来看一下语法:

java
for(元素类型 元素 : 数组或集合){  
// 要执行的代码
}

来看一下示例:

java
String[] strs = {"沉默王二", "一枚有趣的程序员"};

for (String str : strs) {
    System.out.println(str);
}

输出:

text
沉默王二
一枚有趣的程序员

“呀,二哥,你开始王哥卖瓜了啊。”

“嘿嘿,三妹,你这样说哥会脸红的。”

**3)无限 for 循环**

“三妹,你想不想体验一下无限 for 循环的威力,也就是死循环。”

“二哥,那会有什么样的后果啊?”

“来,看看就知道了。”

java
for(;;){
    System.out.println("停不下来。。。。");
}

输出:

text
停不下来。。。。
停不下来。。。。
停不下来。。。。
停不下来。。。。

一旦运行起来,就停不下来了,除非强制停止。

04、while 循环

来看一下 while 循环的格式:

java
while(条件){  
//循环体  
}

画个流程图:

来个示例:

java
int i = 0;
while (true) {
    System.out.println("沉默王三");
    i++;
    if (i == 5) {
        break;
    }
}

“三妹,你猜猜会输出几次?”

“五次吗?”

“对了,你可真聪明。”

text
沉默王三
沉默王三
沉默王三
沉默王三
沉默王三

“三妹,你想不想体验一下无限 while 循环的威力,也就是死循环。”

“二哥,那会有什么样的后果啊?”

“来,看看就知道了。”

java
while (true) {
    System.out.println("停不下来。。。。");
}

输出:

text
停不下来。。。。
停不下来。。。。
停不下来。。。。
停不下来。。。。

把 while 的条件设置为 true,并且循环体中没有 break 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。

05、do-while 循环

来看一下 do-while 循环的格式:

java
do{  
// 循环体
}while(条件);

画个流程图:

来个示例:

java
int i = 0;
do {
    System.out.println("沉默王三");
    i++;
    if (i == 5) {
        break;
    }
} while (true);

“三妹,你猜猜会输出几次?”

“五次吗?”

“对了,你可真聪明。”

text
沉默王三
沉默王三
沉默王三
沉默王三
沉默王三

“三妹,你想不想体验一下无限 do-while 循环的威力......”

“二哥,又来啊,我都腻了。”

“来吧,例行公事,就假装看看嘛。”

java
do {
    System.out.println("停不下来。。。。");
} while (true);

输出:

text
停不下来。。。。
停不下来。。。。
停不下来。。。。
停不下来。。。。

把 do-while 的条件设置为 true,并且循环体中没有 break 关键字的话,程序一旦运行起来,就根本停不下来了,除非强制停止。

06、break

break 关键字通常用于中断循环或 switch 语句,它在指定条件下中断程序的当前流程。如果是内部循环,则仅中断内部循环。

可以将 break 关键字用于所有类型循环语句中,比如说 for 循环、while 循环,以及 do-while 循环。

来画个流程图感受一下:

用在 for 循环中的示例:

java
for (int i = 1; i <= 10; i++) {
    if (i == 5) {
        break;
    }
    System.out.println(i);
}

用在嵌套 for 循环中的示例:

java
for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= 3; j++) {
        if (i == 2 && j == 2) {
            break;
        }
        System.out.println(i + " " + j);
    }
}

用在 while 循环中的示例:

java
int i = 1;
while (i <= 10) {
    if (i == 5) {
        i++;
        break;
    }
    System.out.println(i);
    i++;
}

用在 do-while 循环中的示例:

java
int j = 1;
do {
    if (j == 5) { 
        j++;
        break;
    }
    System.out.println(j);
    j++;
} while (j <= 10);

用在 switch 语句中的示例:

java
switch (age) {
    case 20 :
        System.out.println("上学");
        break;
    case 24 :
        System.out.println("苏州工作");
        break;
    case 30 :
        System.out.println("洛阳工作");
        break;
    default:
        System.out.println("未知");
        break; // 可省略
}

07、continue

当我们需要在 for 循环或者 (do)while 循环中立即跳转到下一个循环时,就可以使用 continue 关键字,通常用于跳过指定条件下的循环体,如果循环是嵌套的,仅跳过当前循环。

来个示例:

java
for (int i = 1; i <= 10; i++) {
    if (i == 5) {
        // 使用 continue 关键字
        continue;// 5 将会被跳过
    }
    System.out.println(i);
}

输出:

text
1
2
3
4
6
7
8
9
10

“二哥,5 真的被跳过了呀。”

“那必须滴。不然就是 bug。”

再来个循环嵌套的例子。

java
for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= 3; j++) {
        if (i == 2 && j == 2) {
            //  当i=2,j=2时跳过
            continue;
        }
        System.out.println(i + " " + j);
    }
}

打印出什么玩意呢?

text
1 1
1 2
1 3
2 1
2 3
3 1
3 2
3 3

“2 2” 没有输出,被跳过了。

再来看一下 while 循环时 continue 的使用示例:

java
int i = 1;
while (i <= 10) {
    if (i == 5) {
        i++;
        continue;
    }
    System.out.println(i);
    i++;
}

输出:

text
1
2
3
4
6
7
8
9
10

注意:如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。

最后,再来看一下 do-while 循环时 continue 的使用示例:

java
int i=1;
do{
    if(i==5){
        i++;
        continue;
    }
    System.out.println(i);
    i++;
}while(i<=10);

输出:

text
1
2
3
4
6
7
8
9
10

注意:同样的,如果把 if 条件中的“i++”省略掉的话,程序就会进入死循环,一直在 continue。

08、小结

本文全面讲解了Java流程控制语句,包括if、switch、while、for等结构。通过学习本文,你将了解到Java流程控制语句的基本概念、语法结构和使用场景,帮助你在实际编程过程中更加灵活地运用各类控制结构。


GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。


File: docs/src/basic-grammar/javadoc.md


title: 教妹学Java:了解Java注释
shortTitle: Java注释
category:

  • Java核心
    tag:
  • Java语法基础
    head:
    • meta
    • name: keywords
      content: Java, 注释, 单行注释, 多行注释, 文档注释

“二哥,Java 中的注释好像真没什么可讲的,我已经提前预习了,不过是单行注释,多行注释,还有文档注释。”三妹的脸上泛着甜甜的笑容,她竟然提前预习了接下来要学习的知识,有一种“士别三日,当刮目相看”的感觉。

“注释的种类确实不多,但还是挺有意思的,且听哥来给你说道说道。”

01、单行注释

单行注释通常用于解释方法内某单行代码的作用。

java
public void method() {
    int age = 18; // age 用于表示年龄
}

但如果写在行尾的话,其实是不符合阿里巴巴的开发规约的。

正确的单行注释如上图中所说,在被注释语句上方另起一行,使用 // 注释。

java
public void method() {
    // age 用于表示年龄
    int age = 18;
}

02、多行注释

多行注释使用的频率其实并不高,通常用于解释一段代码的作用。

java
/*
age 用于表示年纪
name 用于表示姓名
*/
int age = 18;
String name = "沉默王二";

以 /* 开始,以 */ 结束,但不如用多个 // 来得痛快,因为 * 和 / 不在一起,敲起来麻烦。

java
// age 用于表示年纪
// name 用于表示姓名
int age = 18;
String name = "沉默王二";

03、文档注释

文档注释可用在三个地方,类、字段和方法,用来解释它们是干嘛的。

java
/**
 * 微信搜索「沉默王二」,回复 Java
 */
public class Demo {
    /**
     * 姓名
     */
    private int age;

    /**
     * main 方法作为程序的入口
     *
     * @param args 参数
     */
    public static void main(String[] args) {

    }
}

PS:在 Intellij IDEA 中,按下 /** 后敲下回车键就可以自动添加文档注释的格式,*/ 是自动补全的。

接下来,我们来看看如何通过 javadoc 命令生成代码文档。

第一步,在该类文件上右键,找到「Open in Terminal」 可以打开命令行窗口。

第二步,执行 javadoc 命令 javadoc Demo.java -encoding utf-8。-encoding utf-8 可以保证中文不发生乱码。

**第三步,**执行 ls -l 命令就可以看到生成代码文档时产生的文件,主要是一些可以组成网页的 html、js 和 css 文件。

第四步,执行 open index.html 命令可以通过默认的浏览器打开文档注释。

点击「Demo」,可以查看到该类更具体的注释文档。

04、文档注释的注意事项

1)javadoc 命令只能为 public 和 protected 修饰的字段、方法和类生成文档。

default 和 private 修饰的字段和方法的注释将会被忽略掉。因为我们本来就不希望这些字段和方法暴露给调用者。

如果类不是 public 的话,javadoc 会执行失败。

2)文档注释中可以嵌入一些 HTML 标记,比如说段落标记 ``,超链接标记 <a></a> 等等,但不要使用标题标记,比如说 <h1>,因为 javadoc 会插入自己的标题,容易发生冲突。

3)文档注释中可以插入一些 @ 注解,比如说 @see 引用其他类,@version 版本号,@param 参数标识符,@author 作者标识符,@deprecated 已废弃标识符,等等。

05、注释规约

1)类、字段、方法必须使用文档注释,不能使用单行注释和多行注释。因为注释文档在 IDE 编辑窗口中可以悬浮提示,提高编码效率。

比如说,在使用 String 类的时候,鼠标悬停在 String 上时可以得到以下提示。

2)所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。

3)所有的类都必须添加创建者和创建日期。

Intellij IDEA 中可以在「File and Code Templates」中设置。

语法如下所示:

text
/**
* 微信搜索「沉默王二」,回复 Java
* @author 沉默王二
* @date ${DATE}
*/

设置好后,在新建一个类的时候就可以自动生成了。

java
/**
 * 微信搜索「沉默王二」,回复 Java
 *
 * @author 沉默王二
 * @date 2020/11/16
 */
public class Test {
}

4)所有的枚举类型字段必须要有注释,说明每个数据项的用途。

5)代码修改的同时,注释也要进行相应的修改。

“好了,三妹,关于 Java 中的注释就先说这么多吧。”转动了一下僵硬的脖子后,我对三妹说。“记住一点,注释是程序固有的一部分。”

  • 第一、注释要能够准确反映设计思想和代码逻辑;
  • 第二、注释要能够描述业务含 义,使别的程序员能够迅速了解到代码背后的信息。

完全没有注释的大段代码对于阅读者形同 天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看 的,使其能够快速接替自己的工作。


GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。


File: docs/src/basic-grammar/operator.md


title: 教妹学Java:5分钟掌握运算符
shortTitle: Java运算符
category:

  • Java核心
    tag:
  • Java语法基础
    head:
    • meta
    • name: keywords
      content: Java, 运算符, 算术运算符, 关系运算符, 逻辑运算符, 位运算符, 赋值运算符, 运算符用法, 运算符优先级

“二哥,让我盲猜一下哈,运算符是不是指的就是加减乘除啊?”三妹的脸上泛着甜甜的笑容,我想她一定对提出的问题很有自信。

“是的,三妹。运算符在 Java 中占据着重要的位置,对程序的执行有着很大的帮助。除了常见的加减乘除,还有许多其他类型的运算符,来看下面这张思维导图。”

01、算术运算符

算术运算符除了最常见的加减乘除,还有一个取余的运算符,用于得到除法运算后的余数,来串代码感受下。

java
int a = 10;
int b = 5;

System.out.println(a + b);//15
System.out.println(a - b);//5
System.out.println(a * b);//50
System.out.println(a / b);//2
System.out.println(a % b);//0

b = 3;
System.out.println(a + b);//13
System.out.println(a - b);//7
System.out.println(a * b);//30
System.out.println(a / b);//3
System.out.println(a % b);//1

对于初学者来说,加法(+)、减法(-)、乘法(*)很好理解,但除法(/)和取余(%)会有一点点疑惑。在以往的认知里,10/3 是除不尽的,结果应该是 3.333333...,而不应该是 3。相应的,余数也不应该是 1。这是为什么呢?

因为数字在程序中可以分为两种,一种是整型,一种是浮点型(不清楚的同学可以回头看看数据类型那篇),整型和整型的运算结果就是整型,不会出现浮点型。否则,就会出现浮点型。

java
int a = 10;
float c = 3.0f;
double d = 3.0;
System.out.println(a / c); // 3.3333333
System.out.println(a / d); // 3.3333333333333335
System.out.println(a % c); // 1.0
System.out.println(a % d); // 1.0

需要注意的是,当浮点数除以 0 的时候,结果为 Infinity 或者 NaN。

java
System.out.println(10.0 / 0.0); // Infinity
System.out.println(0.0 / 0.0); // NaN

Infinity 的中文意思是无穷大,NaN 的中文意思是这不是一个数字(Not a Number)。

当整数除以 0 的时候(10 / 0),会抛出异常:

text
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.itwanger.eleven.ArithmeticOperator.main(ArithmeticOperator.java:32)

所以整数在进行除法运算时,需要先判断除数是否为 0,以免程序抛出异常。

算术运算符中还有两种特殊的运算符,自增运算符(++)和自减运算符(--),它们也叫做一元运算符,只有一个操作数。

java
int x = 10;
System.out.println(x++);//10 (11)  
System.out.println(++x);//12  
System.out.println(x--);//12 (11)  
System.out.println(--x);//10

一元运算符可以放在数字的前面或者后面,放在前面叫前自增(前自减),放在后面叫后自增(后自减)。

前自增和后自增是有区别的,拿 int y = ++x 这个表达式来说(x = 10),它可以拆分为 x = x+1 = 11; y = x = 11,所以表达式的结果为 x = 11, y = 11。拿 int y = x++ 这个表达式来说(x = 10),它可以拆分为 y = x = 10; x = x+1 = 11,所以表达式的结果为 x = 11, y = 10。

java
int x = 10;
int y = ++x;
System.out.println(y + " " + x);// 11 11

x = 10;
y = x++;
System.out.println(y + " " + x);// 10 11

对于前自减和后自减来说,你可以自己试一把。

02、关系运算符

关系运算符用来比较两个操作数,返回结果为 true 或者 false。

来看示例:

java
int a = 10, b = 20;
System.out.println(a == b); // false
System.out.println(a != b); // true
System.out.println(a > b); // false
System.out.println(a < b); // true
System.out.println(a >= b); // false
System.out.println(a <= b); // true

03、位运算符

在学习位运算符之前,需要先学习一下二进制,因为位运算符操作的不是整型数值(int、long、short、char、byte)本身,而是整型数值对应的二进制。

java
System.out.println(Integer.toBinaryString(60)); // 111100
System.out.println(Integer.toBinaryString(13)); // 1101

从程序的输出结果可以看得出来,60 的二进制是 0011 1100(用 0 补到 8 位),13 的二进制是 0000 1101。

PS:现代的二进制记数系统由戈特弗里德·威廉·莱布尼茨于 1679 年设计。莱布尼茨是德意志哲学家、数学家,历史上少见的通才。

来看示例:

java
int a = 60, b = 13;
System.out.println("a 的二进制:" + Integer.toBinaryString(a)); // 111100
System.out.println("b 的二进制:" + Integer.toBinaryString(b)); // 1101

int c = a & b;
System.out.println("a & b:" + c + ",二进制是:" + Integer.toBinaryString(c));

c = a | b;
System.out.println("a | b:" + c + ",二进制是:" + Integer.toBinaryString(c));

c = a ^ b;
System.out.println("a ^ b:" + c + ",二进制是:" + Integer.toBinaryString(c));

c = ~a;
System.out.println("~a:" + c + ",二进制是:" + Integer.toBinaryString(c));

c = a << 2;
System.out.println("a << 2:" + c + ",二进制是:" + Integer.toBinaryString(c));

c = a >> 2;
System.out.println("a >> 2:" + c + ",二进制是:" + Integer.toBinaryString(c));

c = a >>> 2;
System.out.println("a >>> 2:" + c + ",二进制是:" + Integer.toBinaryString(c));

对于初学者来说,位运算符无法从直观上去计算出结果,不像加减乘除那样。因为我们日常接触的都是十进制,位运算的时候需要先转成二进制,然后再计算出结果。

鉴于此,初学者在写代码的时候其实很少会用到位运算。对于编程高手来说,为了提高程序的性能,会在一些地方使用位运算。比如说,HashMap 在计算哈希值的时候:

java
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

如果对位运算一点都不懂的话,遇到这样的源码就很吃力。所以说,虽然位运算用的少,但还是要懂。

1)按位左移运算符:

java
System.out.println(10<<2);//10*2^2=10*4=40  
System.out.println(10<<3);//10*2^3=10*8=80  
System.out.println(20<<2);//20*2^2=20*4=80  
System.out.println(15<<4);//15*2^4=15*16=240

10<<2 等于 10 乘以 2 的 2 次方;10<<3 等于 10 乘以 2 的 3 次方。

2)按位右移运算符:

java
System.out.println(10>>2);//10/2^2=10/4=2
System.out.println(20>>2);//20/2^2=20/4=5
System.out.println(20>>3);//20/2^3=20/8=2

10>>2 等于 10 除以 2 的 2 次方;20>>2 等于 20 除以 2 的 2 次方。

04、逻辑运算符

逻辑与运算符(&&):多个条件中只要有一个为 false 结果就为 false。

逻辑或运算符(||):多个条件只要有一个为 true 结果就为 true。

java
int a=10;
int b=5;
int c=20;
System.out.println(ab||a<c);//true || true = true

逻辑非运算符(!):用来反转条件的结果,如果条件为 true,则逻辑非运算符将得到 false。

单逻辑与运算符(&):很少用,因为不管第一个条件为 true 还是 false,依然会检查第二个。

单逻辑或运算符(|):也会检查第二个条件。

也就是说,& 和 | 性能不如 && 和 ||,但用法一样:

java
int a=10;
int b=5;
int c=20;
System.out.println(ab|a<c);//true | true = true

05、赋值运算符

赋值操作符恐怕是 Java 中使用最频繁的操作符了,它就是把操作符右侧的值赋值给左侧的变量。来看示例:

java
int a=10;
int b=20;
a+=4;//a=a+4 (a=10+4)  
b-=4;//b=b-4 (b=20-4)  
System.out.println(a);
System.out.println(b);

不过在进行数值的赋值时,需要小点心,比如说下面这种情况:

编译器之所以提示错误,是因为 = 右侧的算术表达式默认为 int 类型,左侧是 short 类型的时候需要进行强转。

java
short a = 10;
short b = 10;
//a+=b;//a=a+b internally so fine
a = (short)(a + b);
System.out.println(a);

除此之外,还会有边界问题,比如说,两个非常大的 int 相乘,结果可能就超出了 int 的范围:

java
int a = Integer.MAX_VALUE;
int b = 10000;
int c = a * b;
System.out.println(c); // -10000

程序输出的结果为 -10000,这个答案很明显不是我们想要的结果,虽然可以通过右侧表达式强转 long 的方法解决:

java
int a = Integer.MAX_VALUE;
int b = 10000;
long c = (long)a * b;
System.out.println(c); // 21474836470000

但尽量不要这样做,结果非常大的时候,尽量提前使用相应的类型进行赋值。

java
long a = Integer.MAX_VALUE - 1;
long b = 10000;
long c = a * b;
System.out.println(c); // 21474836460000

06、三元运算符

三元运算符用于替代 if-else,可以使用一行代码完成条件判断的要求。来看示例:

java
int a=2;
int b=5;
int min=(a<b)?a:b;
System.out.println(min);

如果 ? 前面的条件为 true,则结果为 : 前的值,否则为 : 后的值。

“好了,三妹,关于 Java 运算符就先说这么多吧,你是不是已经清楚了?”转动了一下僵硬的脖子后,我对三妹说。

“差不多,二哥,我需要写点 demo 练习会。”

07、小结

本文全面介绍了Java运算符,包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符等。通过阅读本文,您将了解到Java运算符的分类、用法和优先级,以及如何在实际开发中灵活运用各类运算符提高编程效率。


GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。


File: docs/src/basic-grammar/type-cast.md


title: 教妹学Java:聊聊基本数据类型的转换
shortTitle: Java数据类型转换
category:

  • Java核心
    tag:
  • Java语法基础
    head:
    • meta
    • name: keywords
      content: Java, 数据类型转换, 强制类型转换, 自动类型转换

“三妹,今天我们来聊聊 Java 中的数据类型转换。”我开门见山地对三妹说。

“好啊。”三妹愉快地说。

数据类型转换的目的是确保在表达式求值时,不同类型的数据能够相互兼容。

01、自动类型转换

自动类型转换(自动类型提升)是 Java 编译器在不需要显式转换的情况下,将一种基本数据类型自动转换为另一种基本数据类型的过程。这种转换通常发生在表达式求值期间,当不同类型的数据需要相互兼容时。自动类型转换遵循以下规则:

  • 如果任一操作数是 double 类型,其他操作数将被转换为 double 类型。
  • 否则,如果任一操作数是 float 类型,其他操作数将被转换为 float 类型。
  • 否则,如果任一操作数是 long 类型,其他操作数将被转换为 long 类型。
  • 否则,所有操作数将被转换为 int 类型。

需要注意的是,自动类型转换只发生在兼容类型之间。例如,从较小的数据类型(如 int)到较大的数据类型(如 long 或 double)的转换是安全的,因为较大的数据类型可以容纳较小数据类型的所有可能值。

text
byte -> short -> int -> long -> float -> double
char -> int -> long -> float -> double

下面是一个简单的示例,演示了自动类型转换:

java
int intValue = 5;
double doubleValue = 2.5;

// 自动类型转换:intValue 被转换为 double 类型
double result = intValue * doubleValue;
System.out.println("结果: " + result); // 输出:结果: 12.5

在这个示例中,我们有一个 int 类型的变量 intValue 和一个 double 类型的变量 doubleValue。当我们将它们相乘时,根据自动类型转换的规则,intValue 将被转换为 double 类型,以便将两个 double 类型的操作数相乘。最终结果将是一个 double 类型的值:12.5。

再来举个例子,顾客到超市购物,购买牙膏 2 盒,面巾纸 4 盒。其中牙膏的价格是 10.9 元,面巾纸的价格是 5.8 元,求商品总价格。实现代码如下:

java
float price1 = 10.9f; // 定义牙膏的价格,单精度浮点型float
double price2 = 5.8; // 定义面巾纸的价格,双精度浮点型double
int num1 = 2; // 定义牙膏的数量,整型 int
int num2 = 4; // 定义面巾纸的数量
double res = price1 * num1 + price2 * num2; // 计算总价
System.out.println("一共付给收银员" + res + "元"); // 输出总价

上述代码中首先定义了一个 float 类型的变量存储牙膏的价格,然后定义了一个 double 类型的变量存储面巾纸的价格,再定义两个 int 类型的变量存储物品的数量,最后进行了乘运算以及和运算之后,将结果储存在一个 double 类型的变量中进行输出。

text
一共付给收银员44.99999923706055元

从执行结果看出,float、int 和 double 三种数据类型参与运算,最后输出的结果为 double 类型的数据。这种转换一般称为“表达式中类型的自动提升”。

自动类型提升有好处,但它也会引起令人疑惑的编译错误。例如,下面看起来正确的程序却会引起问题:

java
byte b = 50;

b = b * 2; // Type mismatch: cannot convert from int to byte

如上所示,第二行会报“类型不匹配:无法从int转换为byte”错误。

该程序试图将一个完全合法的 byte 型的值 50*2 存储给一个 byte 型的变量。但是当表达式求值的时候,操作数被自动的提升为 int 型,计算结果也被提升为 int 型。这样表达式的结果现在是 int 型,不强制转换它就不能被赋为 byte 型。

所以应该使用一个显示的强制类型转换,例如:

java
byte b = 50;
b = (byte)(b*2);

这样就能产生正确的值 100。

但如果是下面这样的代码,就不会报错:

java
byte b = 50;
b *= 2;

这是因为 b *= 2 等价于 b = (byte)(b*2),编译器会自动进行强制类型转换。

另外,之前有球友不太理解这样的代码:

java
byte b = 50;

球友认为 50 默认的是 int 类型,大范围类型转换为小范围类型不应该是需要强制转化吗?

我的回答是编译器帮我们自动做了隐式转换,一个值是 int 类型,但是它的值在 byte 类型的取值范围(-127到 128)内,那么编译器会自动将 int 类型转换为 byte 类型。

希望大家能顾理解,自动类型转换并不局限于小类型到大类型,也可以是大类型到小类型,只要值在小类型的取值范围内,编译器就会自动帮我们做隐式转换。

注意:char 类型比较特殊,char 自动转换成 int、long、float 和 double,但 byte 和 short 不能自动转换为 char,而且 char 也不能自动转换为 byte 或 short。

02、强制类型转换

强制类型转换是 Java 中将一种数据类型显式转换为另一种数据类型的过程。与自动类型转换不同,强制类型转换需要程序员显式地指定要执行的转换。强制类型转换在以下情况中可能需要:

  • 将较大的数据类型转换为较小的数据类型。
  • 将浮点数转换为整数。
  • 将字符类型转换为数值类型。

需要注意的是,强制类型转换可能会导致数据丢失或精度降低,因为目标类型可能无法容纳原始类型的所有可能值。因此,在进行强制类型转换时,需要确保转换后的值仍然在目标类型的范围内。

text
double -> float -> long -> int -> char -> short -> byte

以下是一个简单的示例,演示了强制类型转换:

java
double doubleValue = 42.8;

// 强制类型转换:将 double 类型转换为 int 类型
int intValue = (int) doubleValue;
System.out.println("整数值: " + intValue); // 输出:整数值: 42

在这个示例中,我们有一个 double 类型的变量 doubleValue。我们希望将其转换为 int 类型的变量 intValue。为此,我们使用强制类型转换语法,即在要转换的变量之前加上目标类型的括号(如 (int))。

需要注意的是,将 doubleValue 转换为 int 类型时,小数部分将被截断。因此,输出结果将是:Integer value: 42。在这种情况下,精度丢失是可以接受的,但在其他情况下,我们可能需要更加小心地处理类型转换以避免数据丢失。

顾客到超市购物,购买牙膏 2 盒,面巾纸 4 盒。其中牙膏的价格是 10.9 元,面巾纸的价格是 5.8 元,求商品总价格,在计算总价时采用 int 类型的数据进行存储。实现代码如下:

java
float price1 = 10.9f;
double price2 = 5.8;
int num1 = 2;
int num2 = 4;
int res2 = (int) (price1 * num1 + price2 * num2);
System.out.println("一共付给收银员" + res2 + "元");

在上述实例中,有 double 类型、float 类型和 int 类型的数据参与运算,其运算结果默认为 double 类型,题目要求的结果为 int 类型,因为 int 类型的取值范围要小于 double 类型的取值范围,所以需要进行强制类型转换。

text
一共付给收银员44元

小结

考虑下面这几个算式的结果:

java
int a = 1500000000, b = 1500000000;
int sum = a + b;
long sum1 = a + b;
long sum2 = (long)a + b;
long sum3 = (long)(a + b);

“这几个结果应该一样吧,都是求 a+b 的结果嘛。”三妹想当然地得出了她的结论。

“不是的,三妹,你看看这几个结果的值。”我说。

java
-1294967296
-1294967296
3000000000
-1294967296

“这是怎么回事?为什么结果不一样?”三妹有点懵。

“这是因为int 类型的取值范围是 -2147483648~2147483647,而 a 和 b 的值都是 1500000000,和超出了 int 类型的取值范围,所以会出现溢出的情况。”我解释道。

“那为什么 sum2 的结果是 3000000000 呢?”三妹又问。

“这是因为 sum2 的计算过程是先将 a 转换为 long 类型(强制类型转换),然后再和 b 相加(隐式类型转换),此时的结果也是 long 型,而 3000000000 并没有超出 long 型的取值范围。”我说。

  • int sum = a + b,a 和 b 都是 int 类型,所以 a+b 的结果也是 int 类型,但是 a+b 的结果超出了 int 类型的取值范围,所以会出现溢出的情况。
  • long sum1 = a + b,a 和 b 都是 int 类型,于是 a+b 的和也是 int 类型,但超出了 int 的取值范围,所以会出现溢出的情况;如果 a+b 的和没有超出 int 取值范围,其实会将 a+b 的结果隐式转换为 long 类型。
  • long sum2 = (long)a + b,a 是 int 类型,但是 (long)a 将 a 强转为了 long 类型,然后再和 b 相加,此时 b 将隐式提升为 long 型,于是等式右边的结果也是 long 型,而 3000000000 并没有超出 long 型的取值范围。
  • long sum3 = (long)(a + b),a 和 b 都是 int 类型,a+b 的结果也是 int 类型,但超出了 int 的取值范围,所以会出现溢出的情况;即便是外面有一层 long 的强转,但还没有来得及强转,a+b 的结果已经溢出了,所以强转也没用。

那以上,基本上就把 Java 中的基本类型转化讲清楚了。


GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。


File: docs/src/basic-extra-meal/48-keywords.md


title: 5000字速通Java的48个关键字及2个保留字
shortTitle: Java关键字和保留字
category:

  • Java核心
    tag:
  • Java语法基础
    head:
    • meta
    • name: keywords
      content: Java, 关键字, 保留字, 编程基础

“二哥,就之前你给我展示的 Java 代码中,有 public、static、void、main 等等,它们应该都是关键字吧?”三妹的脸上泛着甜甜的笑容,我想她在学习 Java 方面已经变得越来越自信了。

“是的,三妹。Java 中的关键字可不少呢!你一下子可能记不了那么多,不过,先保留个印象吧,对以后的学习会很有帮助。这些小代码都很简单,你可以照着瞧一瞧,感受一下。”

PS:这里我们按照首字母的自然顺序排列来简述一下,了解即可,记不住没关系哦。这些关键字我们在后续的学习中会详细讲解的,直到你搞懂为止。

1、abstract:

用于声明抽象类,以及抽象方法。

java
abstract class Animal {
    abstract void makeSound();

    public void sleep() {
        System.out.println("The animal is sleeping.");
    }
}

class Dog extends Animal {
    void makeSound() {
        System.out.println("The dog barks.");
    }
}

在这个示例中,我们创建了一个名为 Animal 的抽象类,其中包含一个抽象方法 makeSound() 和一个具体方法 sleep()。

2、boolean:

Java 中的一种基本数据类型,表示布尔值,即真(true)或假(false)。boolean 数据类型常用于判断条件、循环控制和逻辑运算等场景。

java
boolean isStudent = true;

if (isStudent) {
    System.out.println("This person is a student.");
} else {
    System.out.println("This person is not a student.");
}

在这个示例中,我们定义了一个 boolean 变量:isStudent。通过 if 语句,我们可以根据这些变量的值进行不同的操作。

3、break:

用于跳出循环结构(如 for、while 和 do-while 循环)或 switch 语句。当遇到 break 语句时,程序将立即跳出当前循环或 switch 语句,继续执行紧跟在循环或 switch 语句后面的代码。

java
for (int i = 0; i < 10; i++) {
    if (i == 5) {
        break;
    }
    System.out.println("i: " + i);
}
System.out.println("Loop ended.");

在这个示例中,我们使用 for 循环遍历 0 到 9 的整数。当 i 等于 5 时,我们使用 break 语句跳出循环。

4、byte:

用于表示一个 8 位(1 字节)有符号整数。它的值范围是 -128(-2^7)到 127(2^7 - 1)。

由于 byte 类型占用的空间较小,它通常用于处理大量的数据,如文件读写、网络传输等场景,以节省内存空间。

java
byte minByte = -128;
byte maxByte = 127;

在这个示例中,我们声明了三个 byte 类型的变量:minByte、maxByte,并分别赋予了不同的值。

5、case:

通常与 switch 语句一起使用。switch 语句允许根据某个变量的值来选择执行不同的代码块。在 switch 语句中,case 用于标识每个可能的值和对应的代码块。

例子我们直接放到 switch 中一起讲。

6、catch:

用于捕获 try 语句中的异常。在 try 块中可能会抛出异常,而在 catch 块中可以捕获这些异常并进行处理。catch 块可以有多个,每个 catch 块可以捕获特定类型的异常。在 catch 块中,可以根据需要进行异常处理,例如输出错误信息、进行日志记录、恢复程序状态等。

java
try {
    int num = Integer.parseInt("abc");
} catch (NumberFormatException e) {
    System.out.println("Invalid number format");
}

这个程序使用 try-catch 语句捕获 NumberFormatException 异常。在 try 块中,尝试将字符串 "abc" 转换为整数类型,由于这个字符串不是有效的数字格式,将会抛出 NumberFormatException 异常。在 catch 块中,捕获到了这个异常,并输出一条错误信息。

7、char:

用于声明一个字符类型的变量。char 类型的变量可以存储任意的 Unicode 字符,可以使用单引号将字符括起来来表示。

java
char c = 'A';

这个程序创建了一个 char 类型的变量 c,并将其赋值为大写字母 A。

8、class:

用于声明一个类。

java
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void sayHello() {
        System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
    }
}

9、continue:

用于继续下一个循环,可以在指定条件下跳过其余代码。

java
for (int i = 1; i <= 10; i++) {
    if (i % 2 == 0) {
        continue;
    }
    System.out.println(i);
}

10、default:

用于指定 switch 语句中除去 case 条件之外的默认代码块。这个我们放到 switch 里一起演示。

11、do:

通常和 while 关键字配合使用,do 后紧跟循环体。

java
int i = 1;
do {
    System.out.println(i);
    i++;
} while (i <= 10);

do-while 循环与 while 循环类似,不同之处在于 do-while 循环会先执行循环体中的代码,然后再检查循环条件。因此,do-while 循环至少会执行一次循环体中的代码。

12、double:

用于声明一个双精度浮点类型的变量。

java
double a = 3.14;
double b = 2.0;
double c = a + b;

13、else:

用于指示 if 语句中的备用分支。

java
int score = 75;
if (score >= 60) {
    System.out.println("及格了");
} else {
    System.out.println("挂科了");
}

14、enum:

用于定义一组固定的常量(枚举)。

java
public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
}

15、extends:

用于指示一个类是从另一个类或接口继承的。

java
class Animal {
    public void eat() {
        System.out.println("动物正在吃东西");
    }
}

class Dog extends Animal {
    public void bark() {
        System.out.println("狗在汪汪叫");
    }
}

public class ExtendsDemo {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();
        dog.bark();
    }
}

Animal 类中有一个 eat() 方法,输出字符串 "动物正在吃东西"。Dog 类继承自 Animal 类,并定义了一个 bark() 方法,输出字符串 "狗在汪汪叫"。

16、final:

用于表示某个变量、方法或类是最终的,不能被修改或继承。

①、final 变量:表示一个常量,一旦被赋值,其值就不能再被修改。这在声明不可变的值时非常有用。

java
final double PI = 3.14159265359;

②、final 方法表示一个不能被子类重写的方法。这在设计类时,确保某个方法的实现不会被子类修改时非常有用。

java
class Animal {
    final void makeSound() {
        System.out.println("动物发出声音.");
    }
}

class Dog extends Animal {
    // 错误: 无法覆盖来自 Animal 的 final 方法
    // void makeSound() {
    //     System.out.println("狗吠叫.");
    // }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.makeSound();
    }
}

③、final 类表示一个不能被继承的类。这在设计类时,确保其不会被其他类继承时非常有用。String 类就是 final 的。

java
final class Animal {
    void makeSound() {
        System.out.println("动物发出声音.");
    }
}

// 错误: 类型 Dog 无法继承 final 类 Animal
// class Dog extends Animal {
//     void makeSound() {
//         System.out.println("狗吠叫.");
//     }
// }

public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.makeSound();
    }
}

17、finally:

和 try-catch 配合使用,表示无论是否处理异常,总是执行 finally 块中的代码。

java
try {
    int x = 10 / 0;  // 抛出异常
} catch (Exception e) {
    System.out.println("发生了异常:" + e.getMessage());
} finally {
    System.out.println("finally 块被执行");
}

18、float:

表示单精度浮点数。

java
float f1 = 3.14f;   // 注意要在数字后面加上 f 表示这是一个 float 类型
float f2 = 1.23e-4f;   // 科学计数法表示小数

在 Java 中,浮点数默认是 double 类型,如果要使用 float 类型的数据,需要在数字后面加上一个 f 或者 F,表示这是一个 float 类型的字面量。另外,也可以使用科学计数法表示浮点数,例如 1.23e-4 表示 0.000123。

19、for:

用于声明一个 for 循环,如果循环次数是固定的,建议使用 for 循环。

java
int[] arr = {1, 2, 3, 4, 5};
for (int i = 0; i < arr.length; i++) {
    System.out.println("arr[" + i + "] = " + arr[i]);
}

20、if:

用于指定条件,如果条件为真,则执行对应代码。

java
int n = -3;
if (n > 0) {
    System.out.println(n + " 是正数");
} else if (n < 0) {
    System.out.println(n + " 是负数");
} else {
    System.out.println(n + " 是零");
}

21、implements:

用于实现接口。

下面是一个实现了 Runnable 接口的类的示例:

java
public class MyThread implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}

22、import:

用于导入对应的类或者接口。例如,如果要使用 Java 标准库中的 ArrayList 类,可以这样写:

java
import java.util.ArrayList;

23、instanceof:

用于判断对象是否属于某个类型(class)。

java
例如,假设有一个 Person 类和一个 Student 类,Student 类继承自 Person 类,可以使用 instanceof 运算符来判断一个对象是否为 Person 类或其子类的实例:
Person p = new Student();
if (p instanceof Person) {
    System.out.println("p is an instance of Person");
}
if (p instanceof Student) {
    System.out.println("p is an instance of Student");
}

24、int:

用于表示整数值。

java
int x;           // 声明一个 int 类型的变量 x
x = 10;          // 将整数值 10 赋给变量 x
int y = 20;     // 声明并初始化一个 int 类型的变量 y,赋值为整数值 20

25、interface:

用于声明接口。会定义一组方法的签名(即方法名、参数列表和返回值类型),但没有方法体。其他类可以实现接口,并提供方法的具体实现。

java
public interface MyInterface {
    void method1();
    int method2(String param);
}

26、long:

用于表示长整数值。

java
long x;           // 声明一个 long 类型的变量 x
x = 10000000000L; // 将长整数值 10000000000 赋给变量 x,需要在数字后面加上 L 或 l 表示这是一个 long 类型的值
long y = 20000000000L; // 声明并初始化一个 long 类型的变量 y,赋值为长整数值 20000000000

27、native:

用于声明一个本地方法,本地方法是指在 Java 代码中声明但在本地代码(通常是 C 或 C++ 代码)中实现的方法,它通常用于与操作系统或其他本地库进行交互。

java
public native void nativeMethod();

28、new:

用于创建一个新的对象。

以下是使用 new 关键字创建对象实例的基本语法:

java
ClassName obj = new ClassName();

以下是使用 new 关键字创建数组实例的基本语法:

java
int[] arr = new int[10];

29、null:

如果一个变量是空的(什么引用也没有指向),就可以将它赋值为 null,和空指针异常息息相关。

java
String str = null; // 声明一个字符串引用,初始化为 null
MyClass obj = null; // 声明一个 MyClass 类型的引用,初始化为 null

30、package:

用于声明类所在的包。

java
package com.example.mypackage;

31、private:

一个访问权限修饰符,表示方法或变量只对当前类可见。

java
public class MyClass {
    private int x; // 私有属性 x,只能在当前类的内部访问

    private void foo() {
        // 私有方法 foo,只能在当前类的内部调用
    }
}

在这个示例中,MyClass 类有一个私有属性 x 和一个私有方法 foo()。这些成员只能在 MyClass 类的内部访问和调用,对其他类不可见。

32、protected:

一个访问权限修饰符,表示方法或变量对同一包内的类和所有子类可见。

java
package com.example.mypackage;

public class MyBaseClass {
    protected int x; // 受保护的属性 x,可以被子类和同一包中的其他类访问

    protected void foo() {
        // 受保护的方法 foo,可以被子类和同一包中的其他类调用
    }
}

package com.example.mypackage;

public class MySubClass extends MyBaseClass {
    public void bar() {
        x = 10; // 可以访问 MyBaseClass 中的受保护属性 x
        foo(); // 可以调用 MyBaseClass 中的受保护方法 foo
    }
}

在这个示例中,MyBaseClass 类有一个受保护的属性 x 和一个受保护的方法 foo()。这些成员可以被子类和同一包中的其他类访问和调用。MySubClass 类继承自 MyBaseClass 类,并可以访问和修改 MyBaseClass 中的受保护成员。

33、public:

一个访问权限修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。main() 方法必须声明为 public。

java
public class MyClass {
    public int x; // 公有属性 x,可以被任何类访问

    public void foo() {
        // 公有方法 foo,可以被任何类调用
    }
}

在这个示例中,MyClass 类有一个公有属性 x 和一个公有方法 foo()。这些成员可以被任何类访问和调用,无论这些类是否在同一个包中。

35、return:

用于从方法中返回一个值或终止方法的执行。return 语句可以将方法的计算结果返回给调用者,或者在方法执行到某个特定条件时提前结束方法。

java
public int add(int a, int b) {
    int sum = a + b;
    return sum; // 返回 sum 的值,并结束方法的执行
}

此外,return 语句还可以用于提前结束方法的执行。例如,假设我们要编写一个方法,用于判断一个整数是否为偶数:

java
public static boolean isEven(int number) {
    if (number % 2 == 0) {
        return true;
    }
    return false;
}

在这个示例中,我们定义了一个名为 isEven 的方法,该方法接收一个整数参数 number。如果 number 是偶数,我们使用 return 语句提前返回 true。否则,方法执行将继续,最后返回 false。

36、short:

用于表示短整数,占用 2 个字节(16 位)的内存空间。

java
short x = 10; // 声明一个 short 类型的变量 x,赋值为 10
short y = 20; // 声明一个 short 类型的变量 y,赋值为 20

37、static:

表示该变量或方法是静态变量或静态方法。

java
public class MyClass {
    public static int x; // 静态变量 x,属于类的成员

    public static void foo() {
        // 静态方法 foo,属于类的成员
    }
}

在这个示例中,MyClass 类有一个静态变量 x 和一个静态方法 foo()。这些成员属于类的成员,可以通过类名直接访问,不需要创建对象。

38、strictfp:

strict floating-point

并不常见,通常用于修饰一个方法,用于限制浮点数计算的精度和舍入行为。当你在类、接口或方法上使用 strictfp 时,该范围内的所有浮点数计算将遵循 IEEE 754 标准的规定,以确保跨平台的浮点数计算的一致性。

不同的硬件平台和 JVM 实现可能对浮点数计算的精度和舍入行为有差异,这可能导致在不同环境中运行相同的浮点数计算代码产生不同的结果。使用 strictfp 关键字可以确保在所有平台上获得相同的浮点数计算结果,避免计算结果的不一致问题。

但请注意,使用 strictfp 可能会对性能产生影响,因为可能需要更多的计算和转换来确保遵循 IEEE 754 标准。因此,在使用 strictfp 时,需要权衡精度和一致性与性能之间的关系。

java
public strictfp class MyClass {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        double result = a + b;
        System.out.println("Result: " + result);
    }
}

输出:

text
Result: 0.30000000000000004

在这个示例中,MyClass 类被声明为 strictfp,因此类中的所有浮点数计算都将遵循 IEEE 754 标准。

在大多数现代操作系统上,使用 strictfp 可能不会产生显著差异,因为大家都遵循 IEEE 754 标准,除非是一些较旧的硬件平台。

IEEE 754 标准(IEEE Standard for Floating-Point Arithmetic)是一个定义浮点数表示和运算的国际标准。由国际电气和电子工程师协会(IEEE)制定,首次发布于1985年。

IEEE 754 标准主要规定了以下几个方面:

浮点数表示:标准定义了两种浮点数格式,单精度(32位)和双精度(64位)。这两种格式分别由符号位、指数位和尾数位组成,用于表示浮点数的大小和精度。

四舍五入和舍入模式:标准定义了多种舍入模式,例如向最接近的数舍入(Round to Nearest, Ties to Even)、向零舍入(Round toward Zero)、向正无穷舍入(Round toward +∞)和向负无穷舍入(Round toward -∞)等。这些模式指导了浮点数计算过程中如何处理精度损失和舍入误差。

特殊值:标准定义了一些特殊的浮点数值,如正无穷(+∞)、负无穷(-∞)和非数值(NaN)。这些特殊值用于表示浮点数计算中可能出现的溢出、下溢和未定义结果等情况。

浮点数运算:标准规定了浮点数的基本运算(加、减、乘、除)和比较运算(等于、不等于、大于、小于、大于等于、小于等于)的行为和结果。这些运算需要遵循标准中规定的表示、舍入和特殊值处理规则。

来看示例

java
public class Ieee754Demo {

    public static void main(String[] args) {
        float a = 0.1f;
        float b = 0.2f;
        float c = a + b;

        System.out.println("a = " + a);
        System.out.println("b = " + b);
        System.out.println("c = a + b = " + c);

        double x = 1.0 / 0.0;
        double y = -1.0 / 0.0;
        double z = 0.0 / 0.0;

        System.out.println("x = 1.0 / 0.0 = " + x);
        System.out.println("y = -1.0 / 0.0 = " + y);
        System.out.println("z = 0.0 / 0.0 = " + z);
    }
}

输出结果:

我们可以看到 IEEE 754 标准中的浮点数表示和运算:

  • 单精度浮点数的加法:变量 a 和 b 分别存储了 0.1 和 0.2,它们的和 c 等于 0.3。由于浮点数表示的精度限制,c 的实际值可能与理论值略有误差。
  • 特殊值:变量 x、y 和 z 分别存储了正无穷(+∞)、负无穷(-∞)和非数值(NaN)。这些特殊值是由除法运算产生的,当被除数为 0 或结果无法表示时,会返回相应的特殊值。

39、super:

可用于调用父类的方法或者字段。

java
class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name); // 调用父类的构造方法
    }

    public void bark() {
        System.out.println(name + " is barking.");
    }

    public void eat() {
        super.eat(); // 调用父类的方法
        System.out.println(name + " is eating bones.");
    }
}

40、switch:

用于根据某个变量的值选择执行不同的代码块。switch 语句通常与 case 和 default 一起使用。每个 case 子句表示一个可能的值和对应的代码块,而 default 子句用于处理不在 case 子句中的值。

java
public class Main {
    public static void main(String[] args) {
        int dayOfWeek = 3;

        switch (dayOfWeek) {
            case 1:
                System.out.println("Monday");
                break;
            case 2:
                System.out.println("Tuesday");
                break;
            case 3:
                System.out.println("Wednesday");
                break;
            case 4:
                System.out.println("Thursday");
                break;
            case 5:
                System.out.println("Friday");
                break;
            case 6:
                System.out.println("Saturday");
                break;
            case 7:
                System.out.println("Sunday");
                break;
            default:
                System.out.println("Invalid day");
                break;
        }
    }
}

在这个示例中,我们定义了一个名为 dayOfWeek 的整数变量,并赋予了一个值。然后,我们使用 switch 语句根据 dayOfWeek 的值来输出对应的星期几。每个 case 子句表示 dayOfWeek 可能的值,后面紧跟着要执行的代码。使用 break 语句跳出 switch 语句,避免执行其他 case 子句的代码。如果 dayOfWeek 的值不在 case 子句中,default 子句将被执行。

41、synchronized:

用于指定多线程代码中的同步方法、变量或者代码块。

java
public class MyClass {
    private int count;

    public synchronized void increment() {
        count++; // 同步方法
    }

    public void doSomething() {
        synchronized(this) { // 同步代码块
            // 执行一些需要同步的操作
        }
    }
}

42、this:

可用于在方法或构造方法中引用当前对象。

java
public class MyClass {
    private int num;

    public MyClass(int num) {
        this.num = num; // 使用 this 关键字引用当前对象的成员变量
    }

    public void doSomething() {
        System.out.println("Doing something with " + this.num); // 使用 this 关键字引用当前对象的成员变量
    }

    public MyClass getThis() {
        return this; // 返回当前对象本身
    }
}

在这个示例中,MyClass 类有一个私有成员变量 num,并定义了一个构造方法、一个方法和一个返回当前对象的方法。在构造方法中,使用 this 关键字引用当前对象的成员变量,并将传入的参数赋值给该成员变量。在方法 doSomething() 中,使用 this 关键字引用当前对象的成员变量,并输出该成员变量的值。在方法 getThis() 中,直接返回当前对象本身。

43、throw:

主动抛出异常。

java
public class MyClass {
    public void doSomething(int num) throws Exception {
        if (num < 0) {
            throw new Exception("num must be greater than zero"); // 手动抛出异常
        }
        // 执行一些操作
    }
}

44、throws:

用于声明异常。

java
public class MyClass {
    public void doSomething(int num) throws Exception {
        if (num < 0) {
            throw new Exception("num must be greater than zero"); // 手动抛出异常
        }
        // 执行一些操作
    }
}

45、transient:

修饰的字段不会被序列化。

java
public class MyClass implements Serializable {
    private int id;
    private String name;
    private transient String password;

    public MyClass(int id, String name, String password) {
        this.id = id;
        this.name = name;
        this.password = password;
    }

    // 省略 getter 和 setter 方法

    @Override
    public String toString() {
        return "MyClass{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

在这个示例中,MyClass 类实现了 Serializable 接口,表示该类的对象可以被序列化。该类有三个成员变量,分别是 id、name 和 password。其中,password 成员变量被标记为 transient,表示在序列化过程中忽略这个成员变量。

45、try:

用于包裹要捕获异常的代码块。

java
try {
    // 可能抛出异常的代码
    int result = 1 / 0;
} catch (Exception e) {
    // 异常处理代码
    e.printStackTrace();
}

46、void:

用于指定方法没有返回值。

java
public void doSomething() {
    // 方法体
}

47、volatile:

保证不同线程对它修饰的变量进行操作时的可见性,即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。

java
public class MyThread extends Thread {
    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {
            // 线程执行的代码
        }
    }

    public void stopThread() {
        running = false;
    }
}

在这个示例中,MyThread 类继承了 Thread 类,重写了 run() 方法。MyThread 类有一个成员变量 running,被标记为 volatile,表示这个变量是共享的,可能会被多个线程同时访问。在 run() 方法中,使用 while 循环检查 running 变量的值,如果 running 为 true,就继续执行循环体中的代码。在另一个方法 stopThread() 中,将 running 变量的值设置为 false,表示需要停止线程。

48、while:

如果循环次数不固定,建议使用 while 循环。

java
int i = 0;
while (i < 10) {
    System.out.println(i);
    i++;
}

“好了,三妹,关于 Java 中的关键字就先说这 48 个吧,这只是一个大概的介绍,后面还会对一些特殊的关键字单独拎出来详细地讲,比如说重要的 static、final 等等,有链接的都是后面会详细讲的。”转动了一下僵硬的脖子后,我对三妹说。

49、goto 和 const:

“除了这些关键字,Java 中还有两个非常特殊的保留字(goto 和 const),它们不能在程序中使用。”

“goto 在 C语言中叫做‘无限跳转’语句,在 Java 中,不再使用 goto 语句,因为无限跳转会破坏程序结构。”

Java 中确实可以使用标签(label)与 break 和 continue 语句结合来实现类似 goto 的跳转功能。以下是一个简单的示例:

java
public class LabelDemo {
    public static void main(String[] args) {
        outerLoop:
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (i == 1 && j == 1) {
                    System.out.println("跳过 outerLoop 中的当前迭代");
                    continue outerLoop;
                }
                System.out.println("i: " + i + ", j: " + j);
            }
        }
        System.out.println("结束");
    }
}

在这个示例中,我们使用了两层嵌套循环。外层循环有一个名为 outerLoop 的标签。当 i 等于 1 且 j 等于 1 时,我们使用 continue outerLoop 语句跳过外层循环中的当前迭代。这与 goto 的行为类似。

来看输出结果:

text
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 1, j: 0
跳过 outerLoop 中的当前迭代
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2
结束

尽管可以使用标签实现类似 goto 的跳转功能,但这种用法在 Java 中仍然较少见,因为过度使用可能导致代码难以理解和维护。通常建议尽可能使用其他控制结构(如 if、for 和 while 语句)来组织代码。

以下是一个使用 if 和 for 语句替代标签跳转的示例。在这个示例中,我们使用了一个布尔变量 skipIteration 来决定是否跳过外层循环的当前迭代:

java
public class IfForDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            boolean skipIteration = false;
            for (int j = 0; j < 3; j++) {
                if (i == 1 && j == 1) {
                    System.out.println("跳过外层循环中的当前迭代");
                    skipIteration = true;
                    break;
                }
                System.out.println("i: " + i + ", j: " + j);
            }
            if (skipIteration) {
                continue;
            }
        }
    }
}

在这个示例中,当 i 等于 1 且 j 等于 1 时,我们将 skipIteration 设置为 true,然后使用 break 语句跳出内层循环。在外层循环中,我们检查 skipIteration 变量的值,如果为 true,则跳过外层循环的当前迭代。

这个示例的输出结果与之前的示例相同:

text
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 1, j: 0
跳过外层循环中的当前迭代
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2

“const 在 C语言中是声明常量的关键字,在 Java 中可以使用 public static final 三个关键字的组合来达到常量的效果。”

java
public class Circle {
    public static final double PI = 3.14159;

    public static double calculateArea(double radius) {
        return PI * radius * radius;
    }
}

在这个示例中,我们使用 public static final 关键字组合定义了一个名为 PI 的常量。因为它是 public 的,所以其他类可以访问这个常量。因为它是 static 的,所以它与类关联,而不是类的实例。因为它是 final 的,所以它的值不能被更改。

“好的二哥,我了解了,你休息会,我再记一记。”


GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。


File: docs/src/basic-extra-meal/annotation.md


title: Java注解,请别小看我。
shortTitle: Java注解
category:

  • Java核心
    tag:
  • Java重要知识点
    head:
    • meta
    • name: keywords
      content: Java,注解,annotation,java 注解,java annotation

“二哥,这节讲注解吗?”三妹问。

“是的。”我说,“注解是 Java 中非常重要的一部分,但经常被忽视也是真的。之所以这么说是因为我们更倾向成为一名注解的使用者而不是创建者。@Override 注解用过吧?方法重写的时候用到过。但你知道怎么自定义一个注解吗?”

三妹毫不犹豫地摇摇头,摆摆手,不好意思地承认自己的确没有自定义过。

“好吧,哥来告诉你吧。”

注解(Annotation)是在 Java 1.5 时引入的概念,同 class 和 interface 一样,也属于一种类型。注解提供了一系列数据用来装饰程序代码(类、方法、字段等),但是注解并不是所装饰代码的一部分,它对代码的运行效果没有直接影响,由编译器决定该执行哪些操作。

来看一段代码。

java
public class AutowiredTest {
    @Autowired
    private String name;

    public static void main(String[] args) {
        System.out.println("沉默王二,一枚有趣的程序员");
    }
}

注意到 @Autowired 这个注解了吧?它本来是为 Spring(后面会讲)容器注入 Bean 的,现在被我无情地扔在了字段 name 的身上,但这段代码所在的项目中并没有启用 Spring,意味着 @Autowired 注解此时只是一个摆设。

“既然只是个摆设,那你这个地方为什么还要用 @Autowired 呢?”三妹好奇地问。

“傻呀你,就是给你举个例子,证明:注解对代码的运行效果没有直接影响,明白我的用意了吧?”我毫不客气地说。

“哦。”三妹若有所思地说。

“认真听哈,接下来给你讲讲注解的生命周期。”我瞅了瞅三妹,看她是否在专注的听,然后继续说,“注解的生命周期有 3 种策略,定义在 RetentionPolicy 枚举中。”

1)SOURCE:在源文件中有效,被编译器丢弃。

2)CLASS:在编译器生成的字节码文件中有效,但在运行时会被处理类文件的 JVM 丢弃。

3)RUNTIME:在运行时有效。这也是注解生命周期中最常用的一种策略,它允许程序通过反射的方式访问注解,并根据注解的定义执行相应的代码。

“然后我们来讲注解装饰的目标。”我看三妹还在线,就继续说。

注解的目标定义了注解将适用于哪一种级别的 Java 代码上,有些注解只适用于方法,有些只适用于成员变量,有些只适用于类,有些则都适用。截止到 Java 9,注解的类型一共有 11 种,定义在 ElementType 枚举中。

1)TYPE:用于类、接口、注解、枚举

2)FIELD:用于字段(类的成员变量),或者枚举常量

3)METHOD:用于方法

4)PARAMETER:用于普通方法或者构造方法的参数

5)CONSTRUCTOR:用于构造方法

6)LOCAL_VARIABLE:用于变量

7)ANNOTATION_TYPE:用于注解

8)PACKAGE:用于包

9)TYPE_PARAMETER:用于泛型参数

10)TYPE_USE:用于声明语句、泛型或者强制转换语句中的类型

11)MODULE:用于模块

“哥,你将这些我都记不住,能不能直接开撸注解呀!!!!!”三妹不耐烦了。

“确实哈,说再多,都不如撸个注解来得让人心动。撸个什么样的注解呢?一个字段注解吧,它用来标记对象在序列化成 JSON 的时候要不要包含这个字段。”我笑着对三妹说,“怎么样?”

“好呀!”

“来看下面这段代码。”

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
    public String value() default "";
}

1)JsonField 注解的生命周期是 RUNTIME,也就是运行时有效。

2)JsonField 注解装饰的目标是 FIELD,也就是针对字段的。

3)创建注解需要用到 @interface 关键字。

4)JsonField 注解有一个参数,名字为 value,类型为 String,默认值为一个空字符串。

“为什么参数名要为 value 呢?有什么特殊的含义吗?”三妹问。

“当然是有的,value 允许注解的使用者提供一个无需指定名字的参数。举个例子,我们可以在一个字段上使用 @JsonField(value = "沉默王二"),也可以把 value = 省略,变成 @JsonField("沉默王二")。”我说。

“那 default "" 有什么特殊含义吗?”三妹继续问。

“当然也是有的,它允许我们在一个字段上直接使用 @JsonField,而无需指定参数的名和值。”我回答说。

“明白了,那 @JsonField 注解已经撸好了,是不是可以使用它了呀?”三妹激动地说。

“嗯,假设有一个 Writer 类,他有 3 个字段,分别是 age、name 和 bookName,后 2 个是必须序列化的字段。就可以这样来用 @JsonField 注解。”我说。

java
public class Writer {
    private int age;

    @JsonField("writerName")
    private String name;

    @JsonField
    private String bookName;

    public Writer(int age, String name, String bookName) {
        this.age = age;
        this.name = name;
        this.bookName = bookName;
    }

    // getter / setter

    @Override
    public String toString() {
        return "Writer{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", bookName='" + bookName + '\'' +
                '}';
    }
}

1)name 上的 @JsonField 注解提供了显式的字符串值。

2)bookName 上的 @JsonField 注解使用了缺省项。

接下来,我们来编写序列化类 JsonSerializer,内容如下:

java
public class JsonSerializer {
    public static String serialize(Object object) throws IllegalAccessException {
        Class<?> objectClass = object.getClass();
        Map<String, String> jsonElements = new HashMap<>();
        for (Field field : objectClass.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(JsonField.class)) {
                jsonElements.put(getSerializedKey(field), (String) field.get(object));
            }
        }
        return toJsonString(jsonElements);
    }

    private static String getSerializedKey(Field field) {
        String annotationValue = field.getAnnotation(JsonField.class).value();
        if (annotationValue.isEmpty()) {
            return field.getName();
        } else {
            return annotationValue;
        }
    }

    private static String toJsonString(Map<String, String> jsonMap) {
        String elementsString = jsonMap.entrySet()
                .stream()
                .map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"")
                .collect(Collectors.joining(","));
        return "{" + elementsString + "}";
    }
}

“JsonSerializer 类的内容看起来似乎有点多啊,二哥,我有点看不懂。”三妹说。

“不要怕,我一点点来解释,直到你搞明白为止。”

1)serialize() 方法是用来序列化对象的,它接收一个 Object 类型的参数。objectClass.getDeclaredFields() 通过反射的方式获取对象声明的所有字段,然后进行 for 循环遍历。在 for 循环中,先通过 field.setAccessible(true) 将反射对象的可访问性设置为 true,供序列化使用(如果没有这个步骤的话,private 字段是无法获取的,会抛出 IllegalAccessException 异常);再通过 isAnnotationPresent() 判断字段是否装饰了 JsonField 注解,如果是的话,调用 getSerializedKey() 方法,以及获取该对象上由此字段表示的值,并放入 jsonElements 中。

2)getSerializedKey() 方法用来获取字段上注解的值,如果注解的值是空的,则返回字段名。

3)toJsonString() 方法借助 Stream 流的方式返回格式化后的 JSON 字符串。Stream 流你还没有接触过,不过没关系,后面我再给你讲。

“现在是不是豁然开朗了?”我问三妹,看到三妹点了点头,我继续说,“接下来,我们来写一个测试类 JsonFieldTest。”

java
public class JsonFieldTest {
    public static void main(String[] args) throws IllegalAccessException {
        Writer cmower = new Writer(18,"沉默王二","Web全栈开发进阶之路");
        System.out.println(JsonSerializer.serialize(cmower));
    }
}

程序输出结果如下:

text
{"bookName":"Web全栈开发进阶之路","writerName":"沉默王二"}

从结果上来看:

1)Writer 类的 age 字段没有装饰 @JsonField 注解,所以没有序列化。

2)Writer 类的 name 字段装饰了 @JsonField 注解,并且显示指定了字符串“writerName”,所以序列化后变成了 writerName。

3)Writer 类的 bookName 字段装饰了 @JsonField 注解,但没有显式指定值,所以序列化后仍然是 bookName。

“怎么样,三妹,是不是也不是特别难?”我对三妹说。

“撸个注解好像真没什么难度,但你接下来的那个 JsonSerializer 我还需要再消化一下。”三妹很认真地说。

“嗯,你好好复习下,我看会《编译原理》。”说完我拿起桌子边上的一本书就走了。


GitHub 上标星 17000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 17000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。