Java-基础
Java基础笔记
Java 反编译
http://javare.cn/
To Be Top Javaer - Java工程师成神之路
http://hollischuang.gitee.io/tobetopjavaer/#/
hollischuang / toBeTopJavaer
https://github.com/hollischuang/toBeTopJavaer
一、概述
Java各版本的区别
简单的说
Java SE 是做桌面端软件的。
Java EE 是用来做网站的(例如我们常见的JSP技术)
Java ME 是做手机软件的。
Java SE (J2SE, Java Platform Standard Edition) 标准版
J2SE是Java的标准版,主要用于开发客户端(桌面应用软件),例如常用的文本编辑器、下载软件、即时通讯工具等,都可以通过J2SE实现。
J2SE包含了Java的核心类库,例如数据库连接、接口定义、输入/输出、网络编程等。
学习Java编程就是从J2SE入手。Java EE (J2EE, Java Platform Enterprise Edition) 企业版
J2EE是功能最丰富的一个版本,主要用于开发高访问量、大数据量、高并发量的网站,例如美团、去哪儿网的后台都是J2EE。
通常所说的JSP开发就是J2EE的一部分。
J2EE包含J2SE中的类,还包含用于开发企业级应用的类,例如EJB、servlet、JSP、XML、事务控制等。
J2EE也可以用来开发技术比较庞杂的管理软件,例如ERP系统(Enterprise Resource Planning,企业资源计划系统)。Java ME (J2ME, Java Platform Micro Edition) 微型版
J2ME 只包含J2SE中的一部分类,受平台影响比较大,主要用于嵌入式系统和移动平台的开发,例如呼机、智能卡、手机(功能机)、机顶盒等。
在智能手机还没有进入公众视野的时候,你是否还记得你的摩托罗拉、诺基亚手机上有很多Java小游戏吗?这就是用J2ME开发的。
Java的初衷就是做这一块的开发。
注意:Android手机有自己的开发组件,不使用J2ME进行开发。
Java5.0版本后,J2SE、J2EE、J2ME分别更名为Java SE、Java EE、Java ME,由于习惯的原因,我们依然称之为J2SE、J2EE、J2ME。
JDK
JDK(Java Development Kit)是一系列工具的集合,这些工具是编译Java源码、运行Java程序所必需的,例如JVM、基础类库、编译器、打包工具等。
JDK下载
https://www.oracle.com/java/technologies/downloads/
页面最下面有Java Archive,点开能下载各个历史版本的JDK,所谓java版本,实际上是指JDK的版本。
JDK所提供的部分工具:
java编译器:javac.exe
java解释器:java.exe
java文档生成器:javadoc.exe
java调试器:jdb.exe
Java文档
Java SE 6 在线中文文档
http://tool.oschina.net/apidocs/apidoc?api=jdk-zh
Java SE 6 官方文档
http://docs.oracle.com/javase/6/docs/api/
Java EE 6 官方文档
http://docs.oracle.com/javaee/6/api/
Java SE 各版本官方文档
https://docs.oracle.com/en/java/javase/index.html
Java SE 8 官方文档
https://docs.oracle.com/javase/8/
Java SE 8 API
https://docs.oracle.com/javase/8/docs/api/index.html
Java 8 Java 命令
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC
Java版本
- **JDK(Java SE Development Kit)**,适用于Java开发者,包括全套开发、调试、监控工具,最全。
- **Server JRE(Server Java Runtime Environment)**,用于服务器部署。
- **JRE(Java Runtime Environment)**,终端用户运行java程序所需。
Java EE/Jakarta EE
2019年 Java Persistence API (JPA) 重命名为 Jakarta Persistence
原因是 Oracle 将 Java EE 交给 Eclipse 基金会了,但由于 Oracle 拥有 Java 注册商标,Eclipse 只能将 Java EE 重命名为 Jakarta EE,这个名字也是票选出来的结果。
Java EE vs J2EE vs Jakarta EE
https://www.baeldung.com/java-enterprise-evolution
jdk9+版本缺少jre(手动生成jre)
新版的 JDK(JDK9,11,13,17)不包含JRE,自有 jdk
手动生成 jre:
进入 jdk/bin 目录执行:
jlink –module-path jmods –add-modules java.desktop –output jre
在 bin 目录下生成 jre 目录,将 jre 目录拷贝到上一层,和 bin 并列
mv jre ../jre
https://blog.csdn.net/u011205527/article/details/127321986
使用 sdkman 管理多 jdk 版本
参考 SDKMAN
Linux 安装 JDK
.rpm版
1、下载 jdk 1.8
https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html
2、安装 jdk 1.8 rpm -i jdk-8u202-linux-x64.rpm
# java -version
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
.bin自解压版
下载官网的 jdk-6u43-linux-x64.bin 自解压文件
给所有用户添加可执行的权限 chmod +x jdk-6u43-linux-x64.bin
执行该文件 ./jdk-6u43-linux-x64.bin
默认装在/usr/java目录下
配置环境变量
配置所有用户共享的jdk环境变量
编辑/etc/profile
export JAVA_HOME=/usr/java/jdk1.6.0_43
export CLASSPATH=$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH:$HOME/bin
让系统重新执行下/etc/profile : source /etc/profile
修改文件后要想马上生效还要运行source /etc/profile
不然只能在下次重进此用户时生效。
tar.gz压缩包
下载官网的 jdk-8u121-linux-x64.tar.gz 压缩包并上传到 linux 目录:/usr/local/
进入 /usr/local/ 目录,tar -xzvf jdk-8u121-linux-x64.tar.gz 解压到当前目录,得到 jdk1.8.0_121 文件夹
配置环境变量
配置所有用户共享的jdk环境变量
编辑 /etc/profile
export JAVA_HOME=/usr/local/jdk1.8.0_121
export CLASSPATH=$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH:$HOME/bin
执行命令 source /etc/profile
使配置生效
Windows中Java环境变量配置
进入环境变量配置窗口,在“用户变量”中,设置3项属性:JAVA_HOME、PATH、CLASSPATH(大小写无所谓),若已存在则点击“编辑”,不存在则点击“新建”:
JAVA_HOME
:设为JDK的安装路径(如C:\Program Files (x86)\Java\jdk1.6.0_45
),此路径下包括lib,bin,jre等文件夹(此变量最好设置,因为以后运行tomcat,eclipse等都需要依靠此变量)。Path
:使得系统可以在任何路径下识别java命令,设为:%JAVA_HOME%\bin
。%JAVA_HOME%
就是引用前面指定的JAVA_HOME变量。CLASSPATH
:Java运行环境加载类的路径,只有类在classpath中,才能被识别和加载,设为.;%JAVA_HOME%\lib
(注意前面的点号(.),点号表示当前路径)。
打开一个CMD窗口,输入java -version
或者javac
命令,看到很多说明信息,证明已经安装并配置成功了。
注意:我配置完用户环境变量后不起作用,重启后也不行,后来改为配置系统环境变量,不需要重启即可生效。
我的java环境变量,系统变量:
JAVA_HOME
:C:\Program Files (x86)\Java\jdk1.6.0_45
Path
: 增加%JAVA_HOME%\bin
,和之前的内容以;
隔开CLASSPATH
:.;%JAVA_HOME%\lib
Mac 安装使用 JDK
Intel 版 Mac 安装 Zulu JDK 11
1、在 azul 官网下载 Zulu JDK 11, 选择 Java 11(LTS), macOS, x86 64-bit, JDK, dmg 下载
Download Azul Zulu Builds of OpenJDK
https://www.azul.com/downloads/?package=jdk
2、得到 zulu11.52.13-ca-jdk11.0.13-macosx_x64.dmg 双击打开按提示安装即可。
3、java -version
查看 jdk 版本
java -version
openjdk version "11.0.13" 2021-10-19 LTS
OpenJDK Runtime Environment Zulu11.52+13-CA (build 11.0.13+8-LTS)
OpenJDK 64-Bit Server VM Zulu11.52+13-CA (build 11.0.13+8-LTS, mixed mode)
M1 版 Mac 安装 Zulu JDK8/11
1、在 azul 官网下载 Zulu JDK,选择 8/11, macOS, ARM 64-bit, JDK, dmg 下载
Download Azul Zulu Builds of OpenJDK
https://www.azul.com/downloads/?package=jdk
2、双击安装 dmg
zulu8.58.0.13-ca-jdk8.0.312-macosx_aarch64.dmg
zulu11.52.13-ca-jdk11.0.13-macosx_aarch64.dmg
3、java -version
查看 jdk 版本
java -version
openjdk version "11.0.13" 2021-10-19 LTS
OpenJDK Runtime Environment Zulu11.52+13-CA (build 11.0.13+8-LTS)
OpenJDK 64-Bit Server VM Zulu11.52+13-CA (build 11.0.13+8-LTS, mixed mode)
程序包 javafx.util 不存在
import javafx.util.Pair;
解决:
单独下载 jfxrt.jar 放入到JDK下的 /jre/lib/ext 目录下
https://stackoverflow.com/questions/55170520/package-javafx-util-does-not-exist
Mac 查看所有已安装的 JDK
打开终端,输入 /usr/libexec/java_home -V
Matching Java Virtual Machines (1): 列出的是已安装的jdk
最后是当前使用的jdk
/usr/libexec/java_home -V
Matching Java Virtual Machines (2):
11.0.13, x86_64: "Zulu 11.52.13" /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
1.8.0_192, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
Mac 使用 JAVA_HOME 环境变量切换不同 JDK 版本
编辑 ~/.zshrc
export JDK8_HOME="/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home"
export JDK11_HOME="/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home"
alias jdk8="export JAVA_HOME=$JDK8_HOME"
alias jdk11="export JAVA_HOME=$JDK11_HOME"
修改保存后执行 source ~/.zshrc
使配置文件立即生效。
之后就可以通过 jdk8, jdk11 两个自定义命令来切换 jdk 版本了。
Mac 使用 JEnv 管理多 JDK 版本
jenv / jenv
https://github.com/jenv/jenv
JAVA_HOME+export+alias 的方式只能整个环境切换 jdk 版本,无法实现在不同目录中使用不同 jdk 版本,比如在 ~/proj1 中就使用 jdk8,在 ~/proj2 中就使用 jdk11,且同时生效。
jenv 是个 jdk 版本管理工具,可实现灵活的目录级的 jdk 版本切换。
1、brew 安装 jenv brew install jenv
brew info jenv
jenv: stable 0.5.4, HEAD
Manage your Java environment
https://www.jenv.be/
/usr/local/Cellar/jenv/0.5.4 (84 files, 73KB) *
Built from source on 2021-11-16 at 13:00:35
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/jenv.rb
License: MIT
==> Options
--HEAD
Install HEAD version
==> Caveats
To activate jenv, add the following to your ~/.zshrc:
export PATH="$HOME/.jenv/bin:$PATH"
eval "$(jenv init -)"
2、根据提示,安装后配置环境变量,zsh 中执行:
echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(jenv init -)"' >> ~/.zshrc
然后执行 exec $SHELL -l
使生效。
然后执行 jenv doctor
,出现 Jenv is correctly loaded 说明 jenv 安装设置成功。
jenv doctor
[OK] No JAVA_HOME set
[ERROR] Java binary in path is not in the jenv shims.
[ERROR] Please check your path, or try using /path/to/java/home is not a valid path to java installation.
PATH : /usr/local/Cellar/jenv/0.5.4/libexec/libexec:/Users/user/.jenv/shims:/Users/user/.jenv/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/Cellar/mysql@5.7/5.7.29/bin:/usr/local/Cellar/mysql@5.7/5.7.29/bin
[OK] Jenv is correctly loaded
3、开启 export 插件,否则不会自动设置 JAVA_HOME
jenv enable-plugin export
exec $SHELL -l
再次执行 jenv doctor
可以看到 JAVA_HOME
已配置
jenv doctor
[OK] JAVA_HOME variable probably set by jenv PROMPT
[OK] Java binaries in path are jenv shims
[OK] Jenv is correctly loaded
echo $JAVA_HOME
/Users/masi/.jenv/versions/1.8
4、将已有 jdk 添加到 jenv 管理/usr/libexec/java_home -V
查看已安装 jdk
/usr/libexec/java_home -V
Matching Java Virtual Machines (2):
11.0.13, x86_64: "Zulu 11.52.13" /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
1.8.0_192, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
使用 jenv add
添加到 jenv
$ jenv add /Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home
oracle64-1.8.0.192 added
1.8.0.192 added
1.8 added
$ jenv add /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
zulu64-11.0.13 added
11.0.13 added
11.0 added
11 added
执行后输出的是 jenv 中的 jdk 别名,比如 oracle64-1.8.0.192, 1.8.0.192, 1.8 指代的都是 jdk8,之后切换 jdk 版本时使用这些别名即可。
5、jenv versions
查看被 jenv 管理的 jdk 版本
jenv versions
* system (set by /Users/user/.jenv/version)
1.8
1.8.0.192
11
11.0
11.0.13
oracle64-1.8.0.192
zulu64-11.0.13
可以看到,jenv 根据 jdk 路径名称生成了多个别名。
版本号左边带星号 *
表示当前执行 java 命令后会使用到的 JDK 版本。
6、配置 shell、local 和 global 级别的 jdk 版本
jenv 提供 shell、local 和 global 三个级别的 jdk 版本配置:
- shell 用于设置终端窗口生命周期内使用的 JDK 版本,一旦结束当前 shell 重新登录会失效。
- local 用于设置当前目录下使用的 JDK 版本
- global 用于设置全局使用的 JDK 版本
jenv global 11
将全局 jdk 配置为 java11,其中最后的 11 是 jenv versions
列出的版本别名
之后 jenv versions
结果为:
jenv versions
system
1.8
1.8.0.192
* 11 (set by /Users/masi/.jenv/version)
11.0
11.0.13
oracle64-1.8.0.192
zulu64-11.0.13
jenv local VERSION
修改当前目录的 jdk 版本,会在当前目录下创建一个名为 .java-version
的文件,之后在当前目录下执行 java 命令时 jenv 会读取此文件并切换 jdk 版本。
使用 IDEA 管理多版本 jdk(推荐)
IDEA 打开任意 java 项目
CMD+; 打开 ProjectStructure 项目设置窗口
Platform Settings -> SDKs 点 加号 -> Download JDK… 直接下载即可
切换 jdk:
CMD+; 打开 ProjectStructure
Project Settings -> Project -> SDK
还是直接使用 IDEA 管理多 jdk 版本方便,切换很快。
一次编译,到处运行
我们编写的Java源码,编译后会生成一种 .class 文件,称为字节码文件。Java虚拟机(Java Virtual Machine,简称JVM)就是负责将字节码文件翻译成特定平台下的机器码然后运行。也就是说,只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。
Java代码首先被编译成字节码文件,再由JVM将字节码文件翻译成机器语言,从而达到运行Java程序的目的。
注意:编译的结果不是生成机器码,而是生成字节码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。
注意:跨平台的是Java程序,不是JVM。JVM是用C/C++开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的JVM。
二、包和类库
源文件声明规则
- 一个源文件中只能有一个public类,一个源文件可以有多个非public类。
- 源文件的名称应该和public类的类名保持一致。例如:源文件中public类的类名是Employee,那么源文件应该命名为Employee.java。
- 如果一个类定义在某个包中,那么package语句应该在源文件的首行。
- 如果源文件包含import语句,那么应该放在package语句和类定义之间。如果没有package语句,那么import语句应该在源文件中最前面。
- import语句和package语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。
包
Java中,为了组织代码的方便,可以将功能相似的类放到一个文件夹内,这个文件夹,就叫做包。
包不但可以包含类,还可以包含接口和其他的包。
包以”.”来表示层级关系,例如引用时 p1.p2.Test 表示的类为 \p1\p2\Test.class。
当包中还有包时,可以使用“包1.包2.…….包n”进行指定,其中,包1为最外层的包,而包n则为最内层的包。
声明包
通过 package 关键字可以声明一个包,例如:
package p1.p2;
public class pclass {
... ...
}
必须将 package 语句放在所有语句的前面
在Java中提供的包,相当于系统中的文件夹。例如,上面代码中的pclass类如果保存到C盘根目录下,那么它的实际路径应该为C:\p1\p2\pclass.java。
调用包中的类
(1) 在每个类名前面加上完整的包名
(2) 通过 import 语句引入包中的类
(3) 同一个包中的类相互调用不需要import
常用包
包命名规则
java.*
开头的是Java的核心包,所有程序都会使用这些包中的类;javax.*
开头的是扩展包,x是extension的意思,也就是扩展。虽然javax是对java的优化和扩展,但是由于javax使用的越来越多,很多程序都依赖于javax,所以javax也是核心的一部分了,也随JDK一起发布。org.*
开头的是各个机构或组织发布的包,因为这些组织很有影响力,它们的代码质量很高,所以也将它们开发的部分常用的类随JDK一起发布。- 自定义包
为了防止重名,有一个惯例:大家都以自己域名的倒写形式作为开头来为自己开发的包命名,例如百度发布的包会以 com.baidu.开头。
可以认为 org.开头的包为非盈利组织机构发布的包,它们一般是开源的,可以免费使用在自己的产品中,不用考虑侵权问题,而以 com.开头的包往往由盈利性的公司发布,可能会有版权问题,使用时要注意。
常用包介绍
- java.lang,该包提供了Java编程的基础类,例如 Object、Math、String、StringBuffer、System、Thread等,不使用该包就很难编写Java代码了。
- java.util,该包提供了包含集合框架、遗留的集合类、事件模型、日期和时间实施、国际化和各种实用工具类(字符串标记生成器、随机数生成器和位数组)。
- java.io,该包通过文件系统、数据流和序列化提供系统的输入与输出。
- java.net,该包提供实现网络应用与开发的类。
- java.sql,该包提供了使用Java语言访问并处理存储在数据源(通常是一个关系型数据库)中的数据API。
- java.awt,java.awt包提供了创建界面和绘制图形图像的所有类
- javax.swing,javax.swing包提供了一组“轻量级”的组件,尽量让这些组件在所有平台上的工作方式相同。
- java.text,提供了与自然语言无关的方式来处理文本、日期、数字和消息的类和接口。
默认导入java.lang包中的类
Java 编译器默认为所有的 Java 程序导入了 JDK 的 java.lang 包中所有的类(import java.lang.*;
),其中定义了一些常用类,如 System、String、Object、Math 等,因此我们可以直接使用这些类而不必显式导入。但是使用其他类必须先导入。
import搜索路径
安装JDK时,我们已经设置了环境变量 CLASSPATH 来指明类库的路径,它的值为 .;%JAVA_HOME%\lib
,而 JAVA_HOME 又为 C:\Program Files (x86)\Java\jdk1.6.0_45
,所以 CLASSPATH 等价于 .;C:\Program Files (x86)\Java\jdk1.6.0_45\lib
执行import p1.Test;
时
Java 运行环境将依次到下面的路径寻找并载入字节码文件 Test.class:
.p1\Test.class(”.”表示当前路径)
C:\Program Files (x86)\Java\jdk1.6.0_45\lib\p1\Test.class
如果在第一个路径下找到了所需的类文件,则停止搜索,否则继续搜索后面的路径,如果在所有的路径下都未能找到所需的类文件,则编译或运行出错。
你可以在CLASSPATH变量中增加搜索路径,例如 .;%JAVA_HOME%\lib;C:\javalib,那么你就可以将类文件放在 C:\javalib 目录下,Java运行环境一样会找到。
三、数据类型
标识符与命名规则
Java标识符
Java标识符由字母、数字、下划线“_”、美元符号“$”或者人民币符号“¥”组成,并且首字母不能是数字。不能把关键字和保留字作为标识符。没有长度限制。对大小写敏感。
命名规则
- 类名:首字母大写,其余小写。如有多个单词组成,则每个单词的首字母大写。例如:Class ImageSpride
- 包名:一个唯一包名的前缀总是全部小写的ASCII字母并且是一个顶级域名,通常是com,edu,gov,mil,net,org,或1981年ISO
3166标准所指定的标识国家的英文双字符代码。包名的后续部分根据不同机构各自内部的命名规范而不尽相同。这类命名规范可能以特定目录名的组成来区分部门(department),项目(project),机器(machine),或注册名(login names)。例如:com.sun.eng, com.apple.quicktime.v2, edu.cmu.cs.bovik.cheese 等; - 接口:命名规则:大小写规则与类名相似。例如:interface RasterDelegate;
- 方法:方法名是一个动词,采用大小写混合的方式,第一个单词的首字母小写,其后单词的首字母大写。例如:run(); runFast();
- 变量:除了变量名外,所有实例,包括类,类常量,均采用大小写混合的方式,第一个单词的首字母小写,其后单词的首字母大写。变量名不应以下划线或美元符号开头,尽管这在语法上是允许的。变量名应简短且富于描述。变量名的选用应该易于记忆,即,能够指出其用途。尽量避免单个字符的变量名,除非是一次性的临时变量。临时变量通常被取名为i,j,k,m和n,它们一般用于整型;c,d,e,它们一般用于字符型。例如:char c; int i;
- 实例变量:大小写规则和变量名相似,除了前面需要一个下划线。int _employeeId; String _name;
- 常量:类常量和ANSI常量的声明,应该全部大写,单词间用下划线隔开。(尽量避免ANSI常量,容易引起错误),例如:
static final int MIN_WIDTH = 4;
static final int MAX_WIDTH = 999;
关键字
- 访问控制:private, protected, public
- 类,方法和变量修饰符:abstract, class, extends, final, implements, interface, native, new, static, strictfp, synchronized, transient, volatile
- 程序控制:break, continue, return, do, while, if, else, for, instanceof, switch, case, default
- 错误处理:try, cathc, throw, throws
- 包相关:import, package
- 基本类型:boolean, byte, char, double, float, int, long, short, null, true, false
- 变量引用:super, this, void
- 保留字:goto, const
null关键字
注意:null是关键字,但NULL不是关键字
Object obj = NULL; // Not Ok
Object obj1 = null //Ok
可以将null赋予任何引用类型,你也可以将null转化成任何类型,但不能将null赋给基本类型变量
instanceof操作符
instanceof是一个二元操作符(运算符),和==类似,判断其左边对象是否为其右边类的实例,返回值为boolean类型。
如果变量引用的是当前类或它的子类的实例,instanceof 返回 true,否则返回 false。
boolean result = object instanceof class
transient关键字
我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
transient使用小结
- 1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
- 2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
- 3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
synchronized关键字
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
Java中synchronized的用法
http://www.importnew.com/21866.html
位运算
Java提供的位运算符有
- 左移
<<
左移右补零,左移n位相当于十进制乘以 2^n - 符号右移
>>
右移左补符号位 - 无符号右移(逻辑右移)
>>>
右移左补零 - 位与
&
- 位或
|
- 位异或
^
- 位非
~
所有位按位取反,包括最左边的符号位
除了 位非 ~
是一元操作符外,其它的都是二元操作符。
由位运算操作符衍生而来的有:&=
按位与赋值|=
按位或赋值^=
按位非赋值>>=
右移赋值>>>=
无符号右移赋值<<=
赋值左移
整型
在Java中,整型数据的长度与平台无关,相反,C/C++ 整型数据的长度是与平台相关的。
无论运行在什么平台上,Java 中的 byte 永远是 1 字节,char占2字节,16位,可在存放汉字,short 是 2 字节,int 是 4 字节,long 是 8 字节(64位)。
Java整型的表示范围
Java 中的 byte 永远是 1 字节,char占2字节,16位,可在存放汉字,short 是 2 字节,int 是 4 字节,long 是 8 字节。
long 最小值是 -9,223,372,036,854,775,808(-2^63),19位十进制
long 最大值是 9,223,372,036,854,775,807(2^63 -1),19位十进制
int 最小值是 -2,147,483,648(-2^31),10位十进制
int 最大值是 2,147,483,647(2^31 -1),10位十进制
Java中处理无符号整形
Java使用补码存储整型
与c/c++不同,Java 不支持无符号类型(unsigned),Java中的所有整型(byte,short,int,long)都是有符号的。
Java 的原始类型里没有无符号类型,如果需要某个宽度的无符号类型,可以用 >>>
无符号右移操作,或者使用下一个宽度的带符号类型来模拟。
>>>
,无符号右移操作符,也叫逻辑右移,左边永远补0>>
,有符号右移操作符,左边补符号位,即如果该数为正,则高位补0,若为负数,则高位补1
Integer.MAX_VALUE
,2147483647,(2^31)-1,的二进制表示为 0111 1111 1111 1111 1111 1111 1111 1111
Integer.MAX_VALUE + 1
,-2147483648,-2^31,的二进制表示为 1000 0000 0000 0000 0000 0000 0000 0000
System.out.println(" "+Integer.MAX_VALUE+": "+Integer.toBinaryString(Integer.MAX_VALUE));
System.out.println((Integer.MAX_VALUE+1)+": "+Integer.toBinaryString(Integer.MAX_VALUE+1));
System.out.println(Integer.MIN_VALUE+": "+Integer.toBinaryString(Integer.MIN_VALUE));
结果:
2147483647: 1111111111111111111111111111111
-2147483648: 10000000000000000000000000000000
-2147483648: 10000000000000000000000000000000
整型扩展规则
窄的整型转换成较宽的整型时符号扩展规则:
如果最初的数值类型是有符号的,那么就执行符号扩展(即如果符号位为1,则扩展为1,如果为零,则扩展为0);
如果它是char,那么不管它将要被提升成什么类型,都执行零扩展。
如何应对 Java 中无符号类型的缺失?
答案就是:使用比要用的无符号类型更大的有符号类型。使用 short 来处理无符号的字节byte,使用 long 来处理无符号整数int等。
例如用int表示无符号byte的方法为:
int unsignedbyte;
byte signedbyte;
unsignedbyte = 0xFF & signedbyte;
首先,把有符号的 byte 提升成 int 类型,然后对这个 int 进行按位与操作,仅保留最后 8 个比特位。因为 Java 中的 byte 是有符号的,所以当一个 byte 的无符号值大于 127 的时候,表示符号的二进制位将被设置为 1(严格来说,这个不能算是符号位,因为在计算机中数字是按照补码方式编码的),对于 Java 来说,这个就是负数。当将负数数值对应的 byte 提升为 int 类型的时候,0 到 7 比特位将会被保留,8 到 31 比特位会被设置为 1。然后将其与 0x000000FF 进行按位与操作来擦除 8 到 31 比特位的 1。上面这句代码可以简短的写作:
Java 自动填充 0xFF 的前导的 0 ,并且在 Java 中,位操作符 & 会导致 byte 自动提升为 int。
对于无符号 int,你需要把它存储到 long 类型中。其他操作和前面类似,只是你需要把 int 提升为 long 然后和 0xFFFFFFFFL 进行按位与操作。最后的 L 用来告诉 Java 请把这个常量视为 long 来处理:
int data;
long longdata;
longdata = data&0x0FFFFFFFFl
八进制/十六进制
八进制有一个前缀 0,例如 010 对应十进制中的 8;
十六进制有一个前缀 0x,例如 0xCAFE;
从 Java 7 开始,可以使用前缀 0b 来表示二进制数据,例如 0b1001 对应十进制中的 9。
同样从 Java 7 开始,可以使用下划线来分隔数字,类似英文数字写法,例如 1_000_000 表示 1,000,000,也就是一百万。下划线只是为了让代码更加易读,编译器会删除这些下划线。
toBinaryString(int i)
public static String toBinaryString(int i)
将十进制转成字符串形式的二进制,不包含前导0
通过方法 Integer.parseUnsignedInt(s, 2)
可将返回的二进制字符串恢复为原整数,即int i == Integer.parseUnsignedInt(Integer.toBinaryString(i), 2)
注意返回的是补码形式的二进制表示,因为 java使用补码存储整数。
System.out.println(Integer.toBinaryString(1));
System.out.println(Integer.toBinaryString(-1));
System.out.println(Integer.toBinaryString(0));
System.out.println(Integer.toBinaryString(Integer.MAX_VALUE));
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE));
System.out.println(8 == Integer.parseUnsignedInt(Integer.toBinaryString(8), 2));
System.out.println(-8 == Integer.parseUnsignedInt(Integer.toBinaryString(-8), 2));
结果
1
11111111111111111111111111111111
0
1111111111111111111111111111111
10000000000000000000000000000000
true
true
bitCount(int i)
public static int bitCount(int i)
返回指定 int 值的二进制补码表示形式的 1 位的数量。此函数有时用于人口普查。
返回:返回指定 int 值的二进制补码表示形式的 1 位的数量。
Java中Integer.bitCount()的源码分析
public static int bitCount(int i) {
// HD, Figure 5-2
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;
}
首先要了解一个运算:对于一个两位的二进制数,他的 “1” 的数量等于二进制数本身 - 二进制的高位
验证:
原值 - 高位 = 1个数
00 - 0 = 0;
01 - 0 = 1;
10 - 1 = 1;
11 - 1 = 10 = 2;
第一行代码 i = i - ((i >>> 1) & 0x55555555);
,的目的就是统计每两位二进制中1的个数,解释如下:i>>>1
是将每两位的高位右移,由于0x55555555 = 0b01010101010101010101010101010101
所以 (i >>> 1) & 0x55555555
是把每2位的高位清零,i - ((i >>> 1) & 0x55555555)
就是 原值 - 每两位的高位,把结果再存储到 i 中。
这时 i 中存储了每两位的统计结果,可以进行两两相加,最后求和。
第二行代码 i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
的目的是把每四位中的高两位和低两位相加,也就是统计每四位中1的个数i >>> 2
是将 每四位 中的 高两位 移到 低两位,和0x33333333 = 0b00110011001100110011001100110011
进行 “与” 操作就是只保留这低两位,然后 (i & 0x33333333) + ((i >>> 2) & 0x33333333)
就是 每四位中的高两位和低两位相加
第三行代码 i = (i + (i >>> 4)) & 0x0f0f0f0f;
是统计每8位中1的个数,也就是每个字节中1的个数
由于这个值最大是 8(0b1000), 只占字节的一半,所以不需要像前两行代码那样分别得出高低位后再运算,运算完再把高四位清零即可
第四行代码 i = i + (i >>> 8);
是统计每两个字节的1个数
第五行代码 i = i + (i >>> 16);
是统计 4 字节的 32 位整型的 高两个字节中1的个数 和 低两个字节中1的个数 之和。
最后 i & 0x3f
是因为 32 位整型中 1 的个数最多是 32 个,也就是 0b100000 最多占 6 个位,所以与 0x3f = 0b00111111
做与运算,把高位的全部清零即可。
二进制 十进制
1 0 1 1 1 1 1 1 1 1 10 11 11 11 11
01 10 10 10 10 1 2 2 2 2
\ / \ / \/ \/
01 0100 0100 1 4 4
\ / \ /
01 1000 1 8
\ / \ /
1001 9
parseInt(String s, int radix)
public static int parseInt(String s, int radix)
使用第二个参数指定的基数,将字符串参数解析为有符号的整数。除了第一个字符可以是用来表示负值的 ASCII 减号 ‘-‘ (‘\u002D’)外,字符串中的字符必须都是指定基数的数字(通过 Character.digit(char, int) 是否返回一个负值确定)。返回得到的整数值。
如果发生以下任意一种情况,则抛出一个 NumberFormatException 类型的异常:
- 第一个参数为 null 或一个长度为零的字符串。
- 基数小于 Character.MIN_RADIX 或者大于 Character.MAX_RADIX。
- 假如字符串的长度超过 1,那么除了第一个字符可以是减号 ‘-‘ (‘u002D’) 外,字符串中存在任意不是由指定基数的数字表示的字符。
- 字符串表示的值不是 int 类型的值。
示例:
parseInt("0", 10) //返回 0
parseInt("473", 10) //返回 473
parseInt("-0", 10) //返回 0
parseInt("-FF", 16) //返回 -255
parseInt("1100110", 2) //返回 102
parseInt("2147483647", 10) //返回 2147483647
parseInt("-2147483648", 10) //返回 -2147483648
parseInt("2147483648", 10) //抛出 NumberFormatException
parseInt("99", 8) //抛出 NumberFormatException
parseInt("Kona", 10) //抛出 NumberFormatException
parseInt("Kona", 27) //返回 411787
参数:
s - 包含要解析的整数表示形式的 String
radix - 解析 s 时使用的基数。
返回:使用指定基数的字符串参数表示的整数。
抛出: NumberFormatException - 如果 String 不包含可解析的 int。
下划线数字字面量
可以在数字字面量中的两位数字之间使用下划线
例如,一个 int 整数 2014 可以写成 2_014 或 20_14 或 201_4
允许以八进制,十六进制和二进制格式使用下划线。
int 下划线大数字使得它们更容易阅读。
在数字字面量中只允许在数字之间使用下划线。
以下示例显示数字字面量中下划线的有效用法:
int x1 = 2_014; // Underscore in deciaml format
int x2 = 2___014; // Multiple consecutive underscores
int x3 = 02_014; // Underscore in octal literal
int x4 = 0b0111_1011_0001; // Underscore in binary literal
int x5 = 0x7_B_1; // Underscores in hexadecimal literal
byte b1 = 1_2_7; // Underscores in decimal format
double d1 = 2_014.01_11; // Underscores in double literal
浮点型float和double
float 是单精度浮点小数,占 4 个字节,有效数字最长为 7 位,有效数字长度包括了整数部分和小数部分
double 是双精度浮点小数,占 8 个字节,有效数字最长为 15 位,有效数字长度包括了整数部分和小数部分
float 类型值后面都有标志 F
或 f
, 大小写无关。
double 类型值后面都有标志 D
或 d
, 大小写无关。
注意:不带任何标志的浮点型数据,系统默认是 double 类型。
如果要将 Float 和 Double 之间相互转型,java 提供了一下方法 Float.doubleValue()
和 Double.floatValue()
为什么float转double误差很大?
double 转 float 不会出现数据误差,而 float 转 double 却误差如此之大?
double d = 3.14;
float f = (float)d;
System.out.println(f);
输出结果是:3.14;
float f = 127.1f;
double d = f;
System.out.println(d);
输出结果是:127.0999984741211
这是因为单精度转双精度的时候,双精度会对单精度进行补位。导致出现偏差。
解决
在单精度转双精度的时候未防止补位问题采用Double.parseDouble(f.floatValue()+"")
Float 和 Double 的 NaN 值
Java 的 Float 和 Double 中都定义了 NaN 值,表示非数字
public static final float NaN = 0.0f / 0.0f;
public static final double NaN = 0.0d / 0.0;
值为 NaN 的 Float 或 Double 变量,toString 结果为字符串 “NaN”
https://www.baeldung.com/java-not-a-number
字符型
char类型占两个字节,可直接赋值为一个汉字,例如char lastName = '张';
Java 中提供的 char 类型和 C 中的 char 有所不同,在 Java 中,chat 是用 2 个字节来表示 Unicode 值,在 C 中,char 是用 1 个字节来表示 ASCII 值。虽然可以在 Java 中把 char 当做无符号短整型来使用,用来表示 0 到 2^16 的整数。但是这样来用可能产生各种诡异的事情,比如当你要打印这个数值的时候实际上打印出来的是这个数值对应的字符而不是这个数值本身的字符串表示
静态数组
数组定义
与C、C++不同,Java在定义数组时并不为数组元素分配内存,因此 []
中无需指定数组元素的个数(数组长度):
int demoArray[];
int[] demoArray;
定义后必须用new为其分配内存空间:demoArray = new int[3];
或者可以定义的同时分配空间:int demoArray[] = new int[3];
数组初始化
可以定义的同时赋初值,或者先定义个引用然后指向数组常量,或者定义并分配空间,等以后再赋值
// int 数组
// 定义的同时赋值
int intArray[] = {1,2,3};
int[] intArray2 = new int[]{1, 2, 3};
// 先定义个引用,然后指向数组常量
int[] intArray3;
intArray3 = new int[] {1,2,3};
// 定义并分配空间,等以后再赋值
int[] intArray4 = new int[4];
// String 数组
// 定义的同时赋值
String stringArray[] = {"我", "是", "谁?"};
String[] stringArray2 = new String[]{"我", "是", "谁?"};
// 先定义个引用,然后指向数组常量
String[] stringArray3;
stringArray3 = new String[]{"我", "是", "谁?"};
// 定义并分配空间,等以后再赋值
String[] stringArray4 = new String[4];
如何构造一个空数组
int[] nil = new int[] {};
int[] nil2 = {};
两种数组初始化方法的区别
String[] str = {"1","2","3"}
与 String[] str = new String[]{"1","2","3"}
的区别String[] str = {"1","2","3"};
这种形式叫 数组初始化式(ArrayInitializer),只能用在声明同时赋值的情况下。String[] str = new String[]{"1","2","3"}
是一般形式的赋值,=
号的右边叫 数组字面量(ArrayLiteral),数组字面量可以用在任何需要一个数组的地方(类型兼容的情况下)。
如:
String[] str = {"1","2","3"}; // 正确的
String[] str = new String[]{"1","2","3"} // 也是正确的
String[] str;
str = {"1","2","3"}; // 编译错误,因为数组初始化式只能用于声明同时赋值的情况下。
str = new String[] {"1","2","3"}; // 正确
又如传参时:
void f(String[] str) {
...
}
f({"1","2","3"}); // 编译错误
f(new String[] {"1","2","3"}); // 正确
又如返回时:
int[] f() {
return {1,2,3,4}; // 编译错误
return new int[]{1,2,3,4}; //正确
return new int[] {}; // 正确,空数组
}
数组的默认值
基本类型数组的元素都有默认值,除char类型外都会初始化为0
引用类型数组的元素默认值都是 null
// 测试数组默认值
@Test
public void testArrayDefaultValue() {
char[] charArray = new char[10];
byte[] byteArray = new byte[10];
short[] shortArray = new short[10];
int[] intArray = new int[10];
long[] longArray = new long[10];
float[] floatArray = new float[10];
double[] doubleArray = new double[10];
Character[] characters = new Character[10];
Byte[] bytes = new Byte[10];
Short[] shorts = new Short[10];
Integer[] integers = new Integer[10];
Long[] longs = new Long[10];
Float[] floats = new Float[10];
Double[] doubles = new Double[10];
String[] strings = new String[10];
System.out.println(Arrays.toString(charArray));
System.out.println(Arrays.toString(byteArray));
System.out.println(Arrays.toString(shortArray));
System.out.println(Arrays.toString(intArray));
System.out.println(Arrays.toString(longArray));
System.out.println(Arrays.toString(floatArray));
System.out.println(Arrays.toString(doubleArray));
System.out.println(Arrays.toString(characters));
System.out.println(Arrays.toString(bytes));
System.out.println(Arrays.toString(shorts));
System.out.println(Arrays.toString(integers));
System.out.println(Arrays.toString(longs));
System.out.println(Arrays.toString(floats));
System.out.println(Arrays.toString(doubles));
System.out.println(Arrays.toString(strings));
}
结果为
[ , , , , , , , , , ]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
[null, null, null, null, null, null, null, null, null, null]
[null, null, null, null, null, null, null, null, null, null]
[null, null, null, null, null, null, null, null, null, null]
[null, null, null, null, null, null, null, null, null, null]
[null, null, null, null, null, null, null, null, null, null]
[null, null, null, null, null, null, null, null, null, null]
[null, null, null, null, null, null, null, null, null, null]
[null, null, null, null, null, null, null, null, null, null]
数组使用
与C、C++不同,Java对数组元素要进行越界检查以保证安全性。
每个数组都有一个length属性来指明它的长度,长度指的是已分配的空间个数,而不是实际使用的空间个数。
foreach遍历
for( arrayType varName: arrayName ){
// Some Code
}
每循环一次,就会获取数组中下一个元素的值,保存到 varName 变量,直到数组结束,例如:
for( int i : intArray ) {
System.out.println(i);
}
二维数组
Java语言中,由于把二维数组看作是数组的数组,数组空间不是连续分配的,所以不要求二维数组每一维的大小相同:
int a[][] = new int[2][ ];
a[0] = new int[3];
a[1] = new int[5];
使用 数组字面量(ArrayLiteral) 初始化二维数组
public merge(int[][] intervals) {
}
merge(new int[][] {{4,5}, {1,4}});
二维数组判空
二维数组为空,要检查三个部分:
一是数组首地址是否为空 array == null
二是是否为 {}
,也就是 array.length == 0
的情况
三是是否为 { {} }
,这时 array.length = 1
,但是 array[0].length == 0
满足上面三个条件中的任意一个,就可以表名二维数组为空。
public boolean isNull(int[][] array) {
if (array == null || array.length == 0 || (array.length == 1 && array[0].length == 0)) {
return true;
}
return false;
}
// 二维数组判空测试
@Test
public void test2DArray() {
System.out.println(isNull(null));
System.out.println(isNull(new int[][]{}));
System.out.println(isNull(new int[][]{{}}));
System.out.println(isNull(new int[][]{{}, {1}}));
}
结果
true
true
true
false
包装类(Wrapper Classes)
Java为每种基本数据类型分别设计了对应的类:
基本数据类型 | 对应的包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
- 装箱:由基本类型向对应的包装类转换称为装箱,例如把 int 包装成 Integer 类的对象;
- 拆箱:包装类向对应的基本类型转换称为拆箱,例如把 Integer 类的对象重新简化为 int;
Java 1.5 之后可以自动拆箱装箱。
函数func(int n)和func(Integer n)构成重载
线上出现过的一个问题,本来调用func(Integer n)时应该传入Integer类型参数,由于编译器版本不同,新来的同事编译后参数类型被改为了Integer.intValue(),调用func(Integer n)时报错说找不到func()函数。后来看了下,我用的jdk1.7,他用的1.6。
四、面向对象
构造方法
JAVA 中的构造函数分为两类:
- 没有参数的构造函数称为默认构造函数。
- 具有参数的构造函数称为参数化构造函数。
1、当类中没有定义构造函数时,系统会指定给该类加上一个空参数的构造函数,这就是默认构造函数。注意当类中自定义了参数化构造函数时,系统不会再给添加默认构造函数。即当类显示定义带参构造函数,而没有显示定义无参的情况,无参构造消失。
如果创建了带参构造器,应该同时定义默认构造器。
因为如果你写了一个带参构造器,那默认构造器就不会自动生成了。所以子类默认构造器在调用父类默认构造器的时候会找不到。
2、在一个类中可以定义多个构造函数,以进行不同的初始化。多个构造函数存在于类中,是以重载的形式体现的。因为构造函数的名称都相同。
构造方法与继承
1、构造函数是不能继承的,只是用来在子类调用,如果父类没有无参构造函数,创建子类时,必须在子类构造函数代码体的第一行显式调用父类的有参数构造函数,否则不能编译,提示 there is no default constructor available in …
2、如果父类没有有参构造函数,那么在创建子类时可以不显式调用父类构造函数,系统会默认调用父类的无参构造函数super();
3、如果父类没有无参构造函数,那系统就调不了默认的无参构造函数了,所以不显示调用编译也就无法通过了;
构造代码块
类中直接用 {} 定义,每一次创建对象时执行。
构造代码块的作用是给对象进行初始化。
构造代码块在对象建立时运行,构造代码块优先于构造函数执行。
构造代码块用于给所有不同构造方法创建的对象进行统一初始化,也就是不论通过本类的哪个构造方法进行构造,都会执行构造代码块中的内容。
class Person {
private String name;
{
System.out.println("在构造方法之前执行");
}
Person() {
}
Person(String name) {
this.name = name;
}
}
Object类
Object 类位于 java.lang 包中,是所有 Java 类的祖先,Java 中的每个类都由它扩展而来。所有类都自动继承Object类,不需要定义时指定。
方法:boolean equals(Object obj)
equals()方法只能比较引用类型,“==”可以比较引用类型及基本类型。
当用 equals() 方法进行比较时,对类 File、String、Date 及包装类来说,是比较类型及内容而不考虑引用的是否是同一个实例。
hashCode()
在同一个应用程序执行期间,对同一个对象调用 hashCode(),必须返回相同的整数结果——前提是 equals() 所比较的信息都不曾被改动过。
如果两个对象被 equals() 方法视为相等,那么对这两个对象调用 hashCode() 必须获得相同的整数结果。
String toString()
在进行 String 与其它类型数据的连接操作时,会自动调用 toString() 方法
Objects工具类
package java.util;
public final class Objects {
}
nonNull
public static boolean nonNull(Object obj) {
return obj != null;
}
继承
继承使用extends
关键字。
单继承性
Java 允许一个类仅能继承一个其它类,即一个类只能有一个父类,这个限制被称做单继承性。
接口允许多继承
构造方法不能被继承
一个类能得到构造方法,只有两个办法:编写构造方法,或者根本没有构造方法,类有一个默认的构造方法。
static关键字
1、静态变量
静态变量不属于任何独立的对象,编译器只为整个类创建一个静态变量的副本,多实例共享该内存。
注意:只要类被装载,静态变量就会被初始化,不管你是否使用了这个静态变量2、静态方法
静态方法不能向对象实施任何操作,所以不能在静态方法中访问实例变量,只能访问类的静态变量
以下情形可以使用静态方法:
一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如 Math.pow())。
一个方法只需要访问类的静态变量。
注意:Java程序的main()方法必须设为public static,在程序启动时还没有任何对象,main()方法是程序的入口。3、静态初始器(静态块)
块是由大括号包围的一段代码。
静态初始器(Static Initializer)是一个存在于类中、方法外面的静态块。
静态初始器仅仅在类装载的时候(第一次使用类的时候)执行一次,往往用来初始化静态变量。4、静态导入
静态导入是 Java 5 的新增特性,用来导入类的静态变量和静态方法
例如:import static packageName.className.methonName; // 导入某个特定的静态方法 import static packageName.className.*; // 导入类中的所有静态成员
final关键字
final所修饰的数据具有“终态”的特征,表示“最终的”意思。
- final 修饰的类不能被继承。
- final 修饰的方法不能被子类重写。
- final 修饰的变量(成员变量或局部变量)即成为常量,只能赋值一次。
- final 修饰的成员变量必须在声明的同时赋值,如果在声明的时候没有赋值,那么只有一次赋值的机会,而且只能在构造方法中显式赋值,然后才能使用。
- final 修饰的局部变量可以只声明不赋值,然后再进行一次性的赋值。
static final
用来修饰成员变量和成员方法,可简单理解为“全局常量”!
对于变量,表示一旦给值就不可修改,并且通过类名可以访问。
对于方法,表示不可覆盖,并且可以通过类名直接访问。
特别要注意一个问题:
对于被static和final修饰过的实例常量,实例本身不能再改变了,但对于一些容器类型(比如,ArrayList、HashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象,这一点在编程中用到很多。
例如:
public class TestStaticFinal {
private static final String strStaticFinalVar = "aaa";
private static String strStaticVar = null;
private final String strFinalVar = null;
private static final int intStaticFinalVar = 0;
private static final Integer integerStaticFinalVar = new Integer(8);
private static final ArrayList<String> alStaticFinalVar = new ArrayList<String>();
private void test() {
System.out.println("-------------值处理前----------\r\n");
System.out.println("strStaticFinalVar=" + strStaticFinalVar + "\r\n");
System.out.println("strStaticVar=" + strStaticVar + "\r\n");
System.out.println("strFinalVar=" + strFinalVar + "\r\n");
System.out.println("intStaticFinalVar=" + intStaticFinalVar + "\r\n");
System.out.println("integerStaticFinalVar=" + integerStaticFinalVar + "\r\n");
System.out.println("alStaticFinalVar=" + alStaticFinalVar + "\r\n");
//strStaticFinalVar="哈哈哈哈"; //错误,final表示终态,不可以改变变量本身.
strStaticVar = "哈哈哈哈"; //正确,static表示类变量,值可以改变.
//strFinalVar="呵呵呵呵"; //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。
//intStaticFinalVar=2; //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。
//integerStaticFinalVar=new Integer(8); //错误, final表示终态,在定义的时候就要初值(哪怕给个null),一旦给定后就不可再更改。
alStaticFinalVar.add("aaa"); //正确,容器变量本身没有变化,但存放内容发生了变化。这个规则是非常常用的,有很多用途。
alStaticFinalVar.add("bbb"); //正确,容器变量本身没有变化,但存放内容发生了变化。这个规则是非常常用的,有很多用途。
System.out.println("-------------值处理后----------\r\n");
System.out.println("strStaticFinalVar=" + strStaticFinalVar + "\r\n");
System.out.println("strStaticVar=" + strStaticVar + "\r\n");
System.out.println("strFinalVar=" + strFinalVar + "\r\n");
System.out.println("intStaticFinalVar=" + intStaticFinalVar + "\r\n");
System.out.println("integerStaticFinalVar=" + integerStaticFinalVar + "\r\n");
System.out.println("alStaticFinalVar=" + alStaticFinalVar + "\r\n");
}
public static void main(String args[]) {
new TestStaticFinal().test();
}
}
super关键字
super关键字与this类似,用来表示父类。
super 也可以用在子类的子类中,super能自动向上层类追溯,直到找到方法的最早定义。
super关键字的功能:
- 调用父类中声明为 private 的变量。
- 调用父类中已经被覆盖了的方法。
- 作为方法名表示父类构造方法。
编译错误:无法将类 A 中的构造器 A应用到给定类型
java重要特性:子类除了拥有自己的特性外还拥有父类的特性。
因此在初始化子类的时候,父类也要被初始化。
比如定义了类
class A {
//这样系统不会为类A自动加上无参的构造函数
public A(int x){}
}
class B extends A {
//这样系统会自动为B加上无参的构造函数,而且在这个构造函数里有一句话super();
}
所以编译时,A编译通过,但是编译B时会提示无法将A中的构造器应用到给定的类型,因为 super()
找不到A中的无参构造器
解决方法:
一、在A中加入无参的构造方法
二、在B中的所有构造方法的第一句话写上super(int);
无法将构造器应用到给定的类型
https://blog.csdn.net/songxueyu/article/details/14447201
多态
方法重载
在一个类中,有相同的函数名称,但形参不同的函数。方法的重载是根据函数的参数列表来决定的,即:参数列表的个数,类型,顺序三个方面。
(1) 声明为final的方法不能被重载
(2) 仅返回类型不同不构成重载。
方法覆盖
如果在新类中定义一个方法,其名称、返回值类型和参数列表正好与父类中的相同,那么,新方法被称做覆盖旧方法
(1) 覆盖方法的返回类型、方法名称、参数列表必须与原方法的相同。子类覆盖父类的方法,在jdk1.5后,参数返回类可以是父类方法返回类的子类,即返回类型可以是父类返回类型的兼容类型。
(2) 被覆盖的方法不能是final类型,因为final修饰的方法是无法覆盖的。
(3) 被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
(4) 被覆盖的方法不能为static。如果父类中的方法为静态的,而子类中的方法不是静态的,但是两个方法除了这一点外其他都满足覆盖条件,那么会发生编译错误;反之亦然。即使父类和子类中的方法都是静态的,并且满足覆盖条件,但是仍然不会发生覆盖,因为静态方法是在编译的时候把静态方法和类的引用类型进行匹配。
(5) 子类覆盖父类方法,可以修改方法作用域修饰符,但只能把方法的作用域放大,而不能把public修改为private,即子类方法不能缩小父类方法的访问权限
子类覆盖父类方法时对于抛出异常的要求
子类覆盖父类方法时,如有异常,子类中抛出的异常与父类中一致或是其子类型,即子类抛出的异常类型不能比父类抛出的异常类型更宽泛。
这也是 里氏替换原则 的一个体现,就是子类不能突然抛出一个父类中都没规定的异常,否则会让使用者无法处理。
如果父类方法抛出多个异常,那么子类在覆盖该方法时,只能抛出父类异常的子集。
如果父类或者接口的方法中没有异常抛出,那么子类在覆盖方法时,也不能抛出异常。
对于构造函数,又有不同:
如果父类的无参构造函数抛出了异常,则子类的无参构造函数不能省略不写,并且必须抛出父类的异常或父类异常的父类,即必须抛出更广泛的异常。
这看似违背了子类抛出的异常类型不能比父类抛出的异常类型更宽泛的原则。但其实并不是这样,因为构造函数没有覆写的概念,只是构造函数间的引用调用而已,即子类的无参构造函数默认调用的是父类的构造函数,所以子类的无参构造函数抛出的异常必须必父类更广泛。
如果是自定义构造函数则没有此限制,因为子类的自定义构造函数并没有调用父类的构造函数。
覆盖和重载的区别
(1) 方法覆盖要求参数列表必须一致,而方法重载要求参数列表必须不一致。
(2) 方法覆盖要求返回类型必须一致,方法重载对此没有要求。
(3) 方法覆盖只能用于子类覆盖父类的方法,方法重载用于同一个类中的所有方法(包括从父类中继承而来的方法)。方法覆盖只存在于子类和父类之间,同一个类中只能重载。
(4) 方法覆盖对方法的访问权限和抛出的异常有特殊的要求,而方法重载在这方面没有任何限制。
(5) 父类的一个方法只能被子类覆盖一次,而一个方法可以在所有的类中可以被重载多次。
运行时多态(动态绑定)
父类引用指向子类对象
多态存在的三个必要条件:要有继承、要有重写、父类变量引用子类对象。
JVM预先为每个类创建了一个方法表(method lable),其中列出了所有方法的名称、参数签名和所属的类
抽象类
只给出方法定义而不具体实现的方法被称为抽象方法,抽象方法是没有方法体的,在代码的表达上就是没有“{}”。
包含一个或多个抽象方法的类也必须被声明为抽象类。
使用 abstract
修饰符来表示抽象方法和抽象类。
- 抽象类除了包含抽象方法外,还可以包含具体的变量和具体的方法。
- 类即使不包含抽象方法,也可以被声明为抽象类,防止被实例化。
- 包含抽象方法的类一定是抽象类。
- 抽象类不能直接使用,必须用子类去实现抽象类,然后使用其子类的实例。
使用场景:
创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例,也就是可以使用抽象类来充当形参,实际实现类作为实参,也就是多态的应用。
接口interface
接口可以看做是一种特殊的抽象类,接口中所有的方法必须都是抽象的,不能有方法体,它比抽象类更加“抽象”。
接口指定一个类必须做什么,而不是规定它如何去做。
接口使用interface
关键字来声明。
- 接口中声明的成员变量默认都是
public static final
的,必须显示的初始化。因而在常量声明时可以省略这些修饰符。 - 接口中只能定义抽象方法,这些方法默认为
public abstract
的,因而在声明方法时可以省略这些修饰符。 - 接口中没有构造方法,不能被实例化。
- 一个接口不实现另一个接口,但可以继承多个其他接口。接口的多继承特点弥补了类的单继承。
类实现接口
类实现接口的关键字为implements
- 如果一个类不能实现该接口的所有抽象方法,那么这个类必须被定义为抽象类。
- 一个类只能继承一个父类,但却可以实现多个接口。
接口作为类型
不允许创建接口的实例,但允许定义接口类型的引用变量,该变量指向了实现接口的类的实例。
使用指向实现接口的类的实例的引用类型可以访问类中所实现的接口中的方法。
抽象类和接口的区别
(1) 抽象类可以为部分方法提供实现,避免了在子类中重复实现这些方法,提高了代码的可重用性,这是抽象类的优势;而接口中只能包含抽象方法,不能包含任何实现。
(2) 一个类只能继承一个直接的父类(可能是抽象类),但一个类可以实现多个接口,这个就是接口的优势。
匿名类
在实际的项目中看到一个很奇怪的现象,Java可以直接new一个接口,然后在new里面粗暴的加入实现代码。就像下面这样。那么问题来了,new出来的对象没有实际的类作为载体,这不是很奇怪吗?
思考以下代码的输出是什么?
Runnable x = new Runnable() {
@Override
public void run() {
System.out.println(this.getClass());
}
};
x.run();
实际答案是出现xxxx$1这样一个类名,它是编译器给定的名称。
匿名类相当于在定义类的同时再新建这个类的实例。
匿名内部类由于没有名字,所以它的创建方式有点儿奇怪。创建格式如下:
new 父类构造器(参数列表)|实现接口() {
//匿名内部类的类体部分
}
浅谈Java的匿名类
https://www.cnblogs.com/caipc/p/5930236.html
java提高篇(十)—–详解匿名内部类
https://www.cnblogs.com/chenssy/p/3390871.html
泛型
所谓“泛型”,就是“宽泛的数据类型”,任意的数据类型。
泛型类
类型参数需要在类名后面给出,类型参数(泛型参数)由尖括号包围,多个参数由逗号分隔。
泛型类定义class className<T1,T2>{ ... }
泛型类在实例化时必须指出具体的类型,也就是向类型参数传值:className variable<T1, T2> = new className<T1, T2>();
泛型方法
泛型方法与泛型类没有必然的联系,泛型方法有自己的类型参数,在普通类中也可以定义泛型方法。
类型擦除
在使用泛型时没有指明数据类型,那么就会擦除泛型类型,编译器会将所有数据向上转型为 Object。
五、异常处理
异常类
当异常情况发生,一个代表该异常的对象被创建并且在导致该错误的方法中被抛出,该方法可以选择自己处理异常或传递该异常。
所有异常类型都是内置类 Throwable 的子类,Throwable 下有 Exception 子类和 Error 子类。
Throwable 重载了 toString() 方法(由 Object 定义),返回一个包含异常描述的字符串,所以在 catch 语句中可以将各种异常 e 的信息输出:System.out.println("Exception: " + e);
Java 异常类层次结构图:
Throwable 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
不受检查的异常
Java 编译器不会检查此类异常,也就是说,当程序中可能出现这类异常,即使没有用 try-catch 语句捕获它,也没有用 throws 子句声明抛出它,也会编译通过。
当然,如果要抛出运行时异常并捕捉后自己处理,也是可以的。
不可查异常(编译器不要求强制处置的异常):包括运行时异常( RuntimeException 与其子类)和错误(Error)。不受检查的异常 = 运行时异常 + 错误
受检查的异常
即必须进行处理的异常,如果不处理,程序就不能编译通过。
受检查的异常包括 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。如 IOException, SQLException 等以及用户自定义的 Exception 异常,一般情况下不自定义检查异常。受检查的异常 = RuntimeException以外的异常
异常处理机制
在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。
- 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
- 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。
对于运行时异常、错误或可查异常,Java 技术所要求的异常处理方式有所不同:
- 运行时异常:由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java 规定,运行时异常将由 Java 运行时系统自动抛出,允许应用程序忽略运行时异常。
- 错误:对于方法运行中可能出现的 Error,当运行方法不欲捕捉时,Java 允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
- 受检查的异常:对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。
异常捕获try/catch
多重catch
当异常被引发时,每一个catch子句被依次检查,第一个匹配异常类型的子句执行。
当一个catch语句执行以后,其他的子句被旁路,执行从try/catch块以后的代码开始继续。
多重catch时,异常子类必须在它们任何父类之前catch,否则子类将永远不会达到。
因为父类的catch语句将捕获该类型及其所有子类类型的异常。
含有无法到达代码时无法编译通过。
嵌套try
如果一个内部的try语句不含特殊异常的catch处理程序,则外层try语句的catch处理程序将检查是否与之匹配。这个过程将继续直到一个catch语句匹配成功,或者是直到所有的嵌套try语句被检查耗尽。如果没有catch语句匹配,Java的运行时系统将处理这个异常。
Java7 try-with-resources
try-with-resources 语句是一个声明一个或多个资源的 try 语句。
一个资源作为一个对象,必须在程序结束之后随之关闭。 try-with-resources 语句确保在语句的最后每个资源都被关闭 。任何实现了 java.lang.AutoCloseable 的对象, 包括所有实现了 java.io.Closeable 的对象, 都可以用作一个资源。
public static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在这个例子中, try-with-resources 语句声明的资源是一个 BufferedReader。声明语句在紧跟在 try 关键字的括号里面。Java SE 7以及后续版本中,BufferedReader类实现了java.lang.AutoCloseable接口。 因为 BufferedReader 实例是在 try-with-resource 语句中声明的, 所以不管 try 语句正常地完成或是 发生意外 (结果就是 BufferedReader.readLine 方法抛出IOException),BufferedReader都将会关闭。
同时 try 多个资源
try (
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("example.png");
OutputStream outputStream = response.getOutputStream();
) {
IOUtils.copy(inputStream, outputStream);
} catch (IOException e) {
log.error("mock 图片读取失败");
}
Java7 catch 多个异常
try {
} catch(IOException | SQLException | Exception ex){
logger.error(ex);
throw new MyException(ex.getMessage());
}
Java SE7新特性之try-with-resources语句
https://blog.csdn.net/jackiehff/article/details/17765909
The try-with-resources Statement
https://docs.oracle.com/javase/7/docs/technotes/guides/language/try-with-resources.html
throws抛出异常
程序可以用throw语句抛出明确的异常,程序执行在throw语句之后立即停止。
所有的Java内置的运行时异常有两个构造函数:一个没有参数,一个带有一个字符串参数,当用到第二种形式时,参数指定描述异常的字符串。
所以可以在抛出异常时携带字符串参数:throw new NullPointerException("demo");
如果对象用作 print( )或println( )的参数时,该字符串被显示。这同样可以通过调用getMessage( )来实现,getMessage( )是由Throwable定义的。
throws子句列举了一个方法可能抛出的所有异常类型。
一个方法可以抛出的所有其他类型的异常必须在throws子句中声明。如果不这样做,将会导致编译错误。
多数从RuntimeException派生的异常类都自动可用,并且它们不需要被包含在任何方法的throws列表中。
throws子句格式
type method-name(parameter-list) throws exception-list{
// body of method
}
exception-list是该方法可以抛出的以有逗号分割的异常类列表
接口抛出异常
- 接口可以声明抛出异常,他的实现可以不抛出异常,调用的时候也会抛出异常
- 接口没有声明抛出异常,它的实现不能抛出异常
- 子类覆盖父类方法时,如有异常,子类中抛出的异常与父类中一致或是其子类型,即子类抛出的异常类型不能比父类抛出的异常类型更宽泛。如果父类方法抛出多个异常,那么子类在覆盖该方法时,只能抛出父类异常的子集。如果父类或者接口的方法中没有异常抛出,那么子类在覆盖方法时,也不能抛出异常。
finally
finally创建一个代码块。该代码块在一个try/catch 块完成之后另一个try/catch出现之前执行。finally块无论有没有异常抛出都会执行。
inally子句是可选项,可以有也可以无。然而每一个try语句至少需要一个catch或finally子句。
使用场景
finally在关闭文件句柄和释放任何在方法开始时被分配的其他资源时是很有用的
返回前也会被调用
一个方法将从一个try/catch块返回到调用程序的任何时候,即使finally块之前有return语句,finally子句在方法返回之前也会被调用。
小结
try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
catch 块:用于处理try捕获到的异常。
finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:
1)在finally语句块中发生了异常。
2)在前面的代码中用了System.exit()退出程序。
3)程序所在的线程死亡。
4)关闭CPU。
深入理解java异常处理机制
http://blog.csdn.net/hguisu/article/details/6155636
e.getMessage()和e.toString()区别
e.toString() 获取的信息包括异常类型和异常详细消息,而 e.getMessage() 只是获取了异常的详细消息字符串。
所以更推荐使用 e.toString()
比如
空指针异常 java.lang.NullPointerException
,e.getMessage() 结果为空,e.toString() 结果为 “java.lang.NullPointerException”
除零异常 java.lang.ArithmeticException
,e.getMessage() 结果为 “/ by zero”,e.toString() 结果为 “java.lang.ArithmeticException: / by zero”
e.printStackTrace(); 会打出详细异常,异常名称,出错位置,便于调试用..
一般一个异常至少几十行
Throwable.getSuppressed() 被抑制异常
Throwable.getSuppressed()
方法返回一个 Throwable[]
数组,包含了所有被抑制的异常。这样,你就可以在异常处理中检查这些被抑制的异常,以获取更全面的错误信息。
try (InputStream is = new FileInputStream("somefile.txt")) {
// 假设这里发生了一个IOException
throw new RuntimeException("An error occurred");
} catch (IOException | RuntimeException e) {
e.printStackTrace();
// 检查并处理被抑制的异常
Throwable[] suppressed = e.getSuppressed();
if (suppressed != null && suppressed.length > 0) {
for (Throwable s : suppressed) {
s.printStackTrace();
}
}
}
在上面的例子中,如果 FileInputStream
的关闭操作(由 try-with-resources
自动管理)在抛出 RuntimeException
时发生了 IOException
,那么这个IOException
就会被抑制。通过调用 getSuppressed()
方法,我们可以获取到这个被抑制的异常,并进行相应的处理。
断言assert
assert格式
assert 布尔表达式
assert 布尔表达式 : 细节描述
如果布尔表达式的值为false,将抛出AssertionError异常,细节描述可填入想展示的信息。
编译
使用命令行编译时要加上参数’-source 1.4’:javac -source 1.4 AssertExample.java
执行
命令行运行时屏蔽断言:java –disableassertions 类名 或 java –da 类名
命令行运行时允许断言:java –enableassertions 类名 或 java –ea 类名
六、输入输出
java.io包中的每一个类都代表了一种特定的输入或输出流。
Java提供了两种类型的输入输出流:一种是面向字节的流,另一种是面向字符的流。
- 字节流
面向字节的流,数据的处理以字节为基本单位;
字节流(Byte Stream)每次读写8位二进制数,也称为二进制字节流或位流。
字节流除了能够处理纯文本文件之外,还能用来处理二进制文件的数据。
InputStream类和OutputStream类是所有字节流的父类。 - 字符流
面向字符的流,用于字符数据的处理;
字符流一次读写16位二进制数,并将其做一个字符而不是二进制位来处理。
Java语言的字符编码采用的是16位的Unicode码。Java 把一个汉字或英文字母作为一个字符对待,回车或换行作为两个字符对待。
Reader和Writer是java.io包中所有字符流的父类。
java系统提供3个可以直接使用的流对象:
System.in(标准输入),通常代表键盘输入。
System.out(标准输出):通常写往显示器。
System.err(标准错误输出):通常写往显示器。
标准输入输出
println() 输出内容后换行,print() 不换行。
Java中没有像C语言中的scanf()语句,从控制台获取输入有点麻烦,推荐使用Scanner类:
Scanner sc = new Scanner(System.in);
System.out.print("输入年份:");
int year = sc.nextInt();
File.separator
String File.separator,与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符
系统默认名称分隔符由系统属性 File.separator 定义,可通过File类的公共静态字段 separator 和 separatorChar 获得。
对于 UNIX 平台,绝对路径名的前缀始终是 “/“。相对路径名没有前缀。表示根目录的绝对路径名的前缀为 “/“ 且名称序列为空。
对于 Microsoft Windows 平台,包含盘符的路径名前缀由驱动器号和一个 “:” 组成。如果路径名是绝对路径名,还可能后跟 “\“。UNC 路径名的前缀是\\\\
;主机名和共享名是名称序列中的前两个名称。没有指定驱动器的相对路径名没有前缀。
面向字符的输入流
面向字符的输入流类都是Reader的子类
Reader类层次结构
Reader类主要子类
类名 | 功能描述 |
---|---|
CharArrayReader | 从字符数组读取的输入流 |
BufferedReader | 缓冲输入字符流 |
PipedReader | 输入管道 |
InputStreamReader | 将字节转换到字符的输入流 |
FilterReader | 过滤输入流 |
StringReader | 从字符串读取的输入流 |
LineNumberReader | 为输入数据附加行号 |
PushbackReader | 返回一个字符并把此字节放回输入流 |
FileReader | 从文件读取的输入流 |
Reader提供的常用方法
方法 | 功能描述 |
---|---|
void close() | 关闭输入流 |
void mark() | 标记输入流的当前位置 |
boolean markSupported() | 测试输入流是否支持 mark |
int read() | 从输入流中读取一个字符 |
int read(char[] ch) | 从输入流中读取字符数组 |
int read(char[] ch, int off, int len) | 从输入流中读 len 长的字符到 ch 内 |
boolean ready() | 测试流是否可以读取 |
void reset() | 重定位输入流 |
long skip(long n) | 跳过流内的 n 个字符 |
使用FileReader类读取文件
FileReader类是InputStreamReader类的子类
import java.io.*;
class ep10_1{
public static void main(String args[]) throws IOException{
char a[]=new char[1000]; //创建可容纳 1000 个字符的数组
FileReader b=new FileReader("ep10_1.txt");
int num=b.read(a); //将数据读入到数组 a 中,并返回字符数
String str=new String(a,0,num); //将字符串数组转换成字符串
System.out.println("读取的字符个数为:"+num+",内容为:\n");
System.out.println(str);
}
}
面向字符的输出流
面向字符的输出流都是类 Writer 的子类
Writer类层次结构:
Writer类的主要子类:
类名 | 功能说明 |
---|---|
CharArrayWriter | 写到字符数组的输出流 |
BufferedWriter | 缓冲输出字符流 |
PipedWriter | 输出管道 |
OutputStreamWriter | 转换字符到字节的输出流 |
FilterWriter | 过滤输出流 |
StringWriter | 输出到字符串的输出流 |
PrintWriter | 包含 print()和 println()的输出流 |
FileWriter | 输出到文件的输出流 |
Writer类提供的常用方法:
方法 | 功能描述 |
---|---|
void close() | 关闭输出流 |
void flush() | 将缓冲区中的数据写到文件中 |
void writer(int c) | 将单一字符 c 输出到流中 |
void writer(String str) | 将字符串 str 输出到流中 |
void writer(char[] ch) | 将字符数组 ch 输出到流 |
void writer(char[] ch, int offset, int length) | 将一个数组内自offset起到length长的字符输出到流 |
使用FileWriter类写入文件:
FileWriter 类是 Writer 子类 OutputStreamWriter 类的子类
import java.io.*;
class ep10_3{
public static void main(String args[]){
try{
FileWriter a=new FileWriter("ep10_3.txt");
for(int i=32;i<126;i++){
a.write(i);
}
a.close();
}
catch(IOException e){}
}
}
面向字节的输入流
面向字节的输入流都是InputStream类的子类
InputStream 类层次结构
InputStream 的主要子类
类名 | 功能描述 |
---|---|
FileInputStream | 从文件中读取的输入流 |
PipedInputStream | 输入管道 |
FilterInputStream | 过滤输入流 |
ByteArrayInputStream | 从字节数组读取的输入流 |
SequenceInputStream | 两个或多个输入流的联合输入流,按顺序读取 |
ObjectInputStream | 对象的输入流 |
LineNumberInputStream | 为文本文件输入流附加行号 |
DataInputStream | 包含读取 Java 标准数据类型方法的输入流 |
BufferedInputStream | 缓冲输入流 |
PushbackInputStream | 返回一个字节并把此字节放回输入流 |
InputStream 提供的常用方法
方法 | 功能描述 |
---|---|
void close() | 关闭输入流 |
void mark() | 标记输入流的当前位置 |
void reset() | 将读取位置返回到标记处 |
int read() | 从当前位置读入一个字节的二进制数据,以此数据为低位字节补足16位的整型量(0~255)返回,若无数据,则返回-1 |
int read(byte b[]) | 从输入流中的当前位置连续读入多个字节保存在数组中,并返回所读取的字节数 |
int read(byte b[], int off, int len) | 从输入流中当前位置连续读len长的字节,从数组第off+1个元素位置处开始存放,并返回所读取的字节数 |
int available() | 返回输入流中可以读取的字节数 |
long skip(long n) | 略过n个字节 |
long skip(long n) | 跳过流内的n个字符 |
boolean markSupported() | 测试输入数据流是否支持标记 |
面向字节的输出流
面向字节的输出流都是OutputStream类的子类
OutputStream 类的层次结构
OutputStream 的主要子类
类名 | 功能描述 |
---|---|
FileOutputStream | 写入文件的输出流 |
PipedOutputStream | 输出管道 |
FilterOutputStream | 过滤输出流 |
ByteArrayOutputStream | 写入字节数组的输出流 |
ObjectOutputStream | 对象的输出流 |
DataOutputStream | 包含写Java标准数据类型方法的输出流 |
BufferedOutputStream | 缓冲输出流 |
PrintStream | 包含print()和println()的输出流 |
OutputStream 提供的常用方法
方法 | 功能描述 |
---|---|
void close() | 关闭输出流 |
void flush() | 强制清空缓冲区并执行向外设输出数据 |
void write(int b) | 将参数b的低位字节写入到输出流 |
void write(byte b[]) | 按顺序将数组b[]中的全部字节写入到输出流 |
void write(byte b[], int off, int len) | 按顺序将数组b[]中第off+1个元素开始的len个数据写入到输出流 |
工具类
八、Java 8+
Java 11 是 2018 年 10 月发布的 LTS(长期支持)版 Java。上一个 LTS 是 6 年前的 Java 8,下一个是明年十月份的 Java 17。
Java 11 相对于 Java 8 的改进
Java 8 已经相对落伍,例如 G1 GC 还不支持并行 Full GC,语言本身也缺乏局部变量的类型推断等现代语言的语法。
Java 9-11 在语言、虚拟机等方面都引入了很多新特性,给开发与服务运行带来很多便利:
- 更成熟的 G1 GC(Java 10 引入并行 Full GC),Java 8 中的 G1 GC 还不支持并行 Full GC
- 局部变量的类型推断(Java 10 引入)
- 统一便利的集合字面值:List.of()、Set.of()、Map.of()、Map.ofEntries()(Java 9 引入)
- JShell:便于验证 / 测试小段代码的 REPL(Java 9 引入)
- 支持 HTTP/2 的响应式异步 HTTP 客户端(Java 9 孵化,Java 11 正式)
Java 11 收费问题
Java 11 并不收费,收费的是 Oracle JDK 11,并且不止 Oracle JDK 11 收费,11 以上版本的 Oracle JDK 都收费,2019 年 4 月起的 Oracle JDK 8(8u211/8u212 起)也都收费。
Oracle JDK 收费,大家可以用 OpenJDK,但是 Oracle 官方对 OpenJDK 每个版本只维护 6 个月(包括 LTS 版)。
推荐的Open JDK发行版
无论 Java 8 还是 Java 11,都推荐选用第三方维护的免费 OpenJDK 发行版,推荐:
Azul 的 Zulu OpenJDK 首选推荐
https://www.azul.com/downloads/zulu-community/?architecture=x86-64-bit&package=jdk
Sap 的 SapMachine
https://sap.github.io/SapMachine/
BellSoft 的 Liberica
https://bell-sw.com/
Amazon 的 Corretto
https://aws.amazon.com/cn/corretto/
以上四款均已通过 Java Compatibility Kit (JCK) 认证,并且版本支持比较全面,故而推荐。
而以下几款仅供参考:
AdoptOpenJDK :未进行 JCK 认证,同时提供采用 HotSpot 与 OpenJ9 两种不同虚拟机的 OpenJDK
https://adoptopenjdk.net/
RedHat 的 OpenJDK :官网只提供 Windows 版本下载并需要注册,Linux 版随 RHEL 及其衍生版发行
https://developers.redhat.com/products/openjdk/overview
阿里巴巴的 Dragonwell:未进行 JCK 认证,目前Java 8 只有的 Linux 正式版与 Windows 实验版,还没有 Mac 版,而 Java 11 只有 Linux 版
https://www.aliyun.com/product/dragonwell
腾讯 Kona:未进行 JCK 认证,目前只有 Java 8 的 Linux 正式版,Java 11 尚在规划中
https://cloud.tencent.com/product/tkjdk
多Java版本并存
无论 Windows、macOS、Linux 均可安装多个版本的 JDK(包括 OpenJDK,下同)。
当然如果用安装包安装的(而不是直接解压版的),系统默认的 java 与 javac 会指向最新版本的 JDK。
在 IDEA 中可以配置多个版本的 JDK,并且可以为每个项目单独选择 JDK 版本。自 IDEA 2020 起,添加 JDK 的地方可以选让 IDEA 代为下载。
兼容性问题
在 Java 9、10、11 刚刚推出时,第三方库未能及时更新的时候确实会有比较多的兼容性问题。而如今大多数第三方库只要更新到最近版本或者较新版本大都没什么问题。因此在新项目中使用 Java 11 通常没问题。
既有项目需要评估升级依赖到最新版或者能支持 Java 11 的较新版本的兼容性,对于相对单纯的 Spring Boot 项目与 Vert.x 项目来说都问题不大,可能升级依赖后只需少数修改即可支持 Java 11。
而对于 Spring Cloud 老版本可能确实比较困难,其中的组件升级到最新版会有不少兼容性问题。对于依赖于其他明确声称不支持 Java 9-11、久未更新或者冷门的第三方依赖的项目,可能也会遇到兼容性问题。
建议新项目优先选用 Java 11,既有项目升级到 Java 11 要谨慎并充分测试。
项目配置
IDEA 配置
Project Structure… -> Platform Settings -> SDKs 点 + 添加本地安装的 OpenJDK 11 或让 IDEA 2020 代为下载 OpenJDK 11。
Project Structure… -> Project Settings -> Project 在 Project SDK 中选择配置好的 OpenJDK 11 SDK。
maven 项目同时兼容 Java 8 与 Java 11
在 pom.xml 配置中加入:
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
然后在 mvn 构建时,根据当前 JDK 版本传 -Djava.version=1.8
或 -Djava.version=11
。
Maven 项目升级到 11
将上述 ${java.version}
改为写死的 11 即可。升级语言版本到 11 之后才可以用新的语言特性,如 var、List.of() 等。
Gradle 项目升级到 11
在 build.gradle 或 build.gradle.kts 中加入
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
升级语言版本到 11 之后可以用新的语言特性,如 var、List.of() 等。
Java 11
什么时候适合用 var 声明变量
参考官方的本地变量类型引用风格指南
Local Variable Type Inference: Style Guidelines
http://openjdk.java.net/projects/amber/LVTIstyle.html
简要来说如下以下场景从代码正确性上要求需要显式指定类型:
1、语法上必须显式类型、不能用 var:
- 函数参数与返回值
- 字段类型
- 类型参数
- 直接用无类型的 null 作为初值
- 直接用无类型的 lambda 表达式或方法引用作初值
2、语法正确,但可能推断结果不符合预期:
- 调用无法推断出具体类型泛型方法且未指定泛型参数
- new 一个泛型类时使用了钻石符号
<>
其他场景与正确性无关,请参见风格指南,会发现大多数局部变量都适合用 var 声明。
不可变集合的使用
切换 Java 11 后推荐使用不可变集合,比如创建初始化后无需修改集合的字面值应该用 List.of()
, Set.of()
, Map.of()
或者 Map.ofEntries()
而不是 new 一个可变集合然后挨个往里塞数据。再如由 Stream 收集成集合时,应该优先选用 Collectors.toUnmodifiableList()
, Collectors.toUnmodifiableSet()
, Collectors.toUnmodifiableMap()
而不是 Collectors.toList()
等。
但是有一点特别需要注意的是,不可变集合不支持 null 值。这表现在以下几点:
创建不可变集合字面值时,元素不能为 null。如果一定需要在可变集合中报错可选值,可以考虑用 Optional 或者用一个非空默认值。
Stream 收集采用
Collectors.toUnmodifiableList()
等时,元素不能有 null。
如果有,需要在collect()
之前filter(Objects::nonNull)
或map(Optional::ofNullable)
再收集。
便于构建Map.Entry
的工厂方法Map.entry()
的 k、v 参数均不能为 null,因此用Collectors.toUnmodifiableMap()
收集之前要确保 k、v 均不会出现 null 值。调用不可变集合的
contains()
,containsAll()
,containsKey()
,containsValue()
等一系列方法时,参数也不能为 null 值,这点很容易忽略,务必特别留意。
表面看来,继续用可变集合就能避免这些烦恼?但是减少可空值的使用能避免很多难以预料的 bug。因此长远来看,还是推荐使用不可变集合,只是在将原本采用可变集合的代码迁移到采用不可变集合时需要仔细核对上述几点。
Java 14
switch 表达式(JEP 361)
JEP 361: Switch Expressions
https://openjdk.org/jeps/361
Switch 表达式于 2017 年 12 月由 JEP 325 提出。JEP 325 作为预览功能在 2018 年 8 月的 JDK 12 中推出。JEP 325 中重载了 break 语句以便从 switch 表达式返回结果值。
对 JDK 12 的反馈表明使用 break 返回数据令人困惑。为响应反馈,JEP 354 作为 JEP 325 的后续,提出了一个新的声明 yield 用来从 switch 中返回数据,并恢复了 break 的原意。
JEP 354 于 2019 年 6 月在 JDK 13 中作为预览功能推出。对 JDK 13 的反馈表明 switch 表达式已准备好在 JDK 14 中成为最终和永久的,无需进一步更改。
java 中的 switch 有个很容易用错的地方,就是 switch 标签间的默认控制流(fall through),case 里忘了写 break 就会导致执行完后继续执行下一个 case。
JEP 361 带来 switch 表达式:
- 新的 switch 标签
case L ->
,表示如果标签匹配,则仅执行标签右侧的代码。 - 每个 case 允许多个常量
- 整个 switch 可以作为一个表达式
- 使用 yield 在 case 中返回结果
Java 14 之前:
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
Java 14 switch 表达式:
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> System.out.println(7);
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}
整个 switch 可以作为一个表达式,使用 yield 在 case 中返回结果:
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
default -> {
String s = day.toString();
int result = s.length();
yield result; // 必须用 yield,用 return 会编译报错 Return outside of enclosing switch expression
}
};
Java 17
instance of 模式匹配变量
// Old code
if (o instanceof String) {
String s = (String)o;
... use s ...
}
// New code
if (o instanceof String s) {
... use s ...
}
instance of 模式匹配 + switch case
JEP 406: Pattern Matching for switch
https://openjdk.org/jeps/406
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
switch case null 匹配
static void testFooBar(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
switch case null+类型匹配
static void testStringOrNull(Object o) {
switch (o) {
case null, String s -> System.out.println("String: " + s);
}
}
optional.ifPresentOrElse
InaccessibleObjectException
报错:
java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not “opens java.lang” to unnamed module @1941a8ff
添加 java.lang 模块
以及
Unable to make field static final java.lang.invoke.MethodHandles$Lookup java.lang.invoke.MethodHandles$Lookup.IMPL_LOOKUP accessible: module java.base does not “opens java.lang.invoke” to unnamed module
添加 java.lang.invoke 模块
原因:
JDK9 以上模块不能使用反射去访问非公有的成员/成员方法以及构造方法,除非模块标识为opens去允许反射访问
解决:
启动参数添加
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/java.text=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/jdk.internal.access=ALL-UNNAMED
--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
--add-opens java.desktop/java.awt.image=ALL-UNNAMED
--add-opens java.base/java.lang.invoke=ALL-UNNAMED
在 IDEA 中可以在运行配置中的 VM options 中添加
https://blog.csdn.net/qq_27525611/article/details/108685030
InaccessibleObjectException java.awt.image
报错:
2023-12-10 18:44:17.328 [http-nio-8001-exec-2] WARN org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.warn:107 - Failed to evaluate Jackson deserialization for type [[simple type, class com.masikkk.blog.api.vo.request.OcrRequest]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type `java.awt.image.BufferedImage`: Failed to call `setAccess()` on Field 'colorModel' (of class `java.awt.image.BufferedImage`) due to `java.lang.reflect.InaccessibleObjectException`, problem: Unable to make field private java.awt.image.ColorModel java.awt.image.BufferedImage.colorModel accessible: module java.desktop does not "opens java.awt.image" to unnamed module @4738a206
解决:
jvm 启动参数添加:
--add-opens java.desktop/java.awt.image=ALL-UNNAMED
上一篇 Java-字符串
下一篇 FFmpeg
页面信息
location:
protocol
: host
: hostname
: origin
: pathname
: href
: document:
referrer
: navigator:
platform
: userAgent
: