001 - Java基础
2024-03-28 10:50:46 7 举报
AI智能生成
111
作者其他创作
大纲/内容
Java开发入门
什么是Java?
Java是Sun Microsystems于1995年首次发布的一种编程语言和计算平台。
编程语言
计算平台
是在电脑中运行应用程序(软件)的环境,包括硬件环境和软件环境。
一般系统平台 包括一台电脑的硬件体系结构、操作系统、运行时库。
Java是快速,安全和可靠的。
从笔记本电脑到数据中心,从游戏机到科学超级计算机,从手机到互联 网,Java无处不在!
Java主要分为三个版本
• JavaSE(J2SE)(Java2 Platform Standard Edition, java平台标准版)
• JavaEE(J2EE)(Java 2 Platform,Enterprise Edition, java平台企业版)
• JavaME(J2ME)(Java 2 Platform Micro Edition, java平台微型版)。
Java之父-James Gosling
1990年Sun公司成立了由James Gosling领导的开发小组,开始致力于开发一种可移植的、跨平台的语言
该语言能生成正确运行于各种操作系统、各种CPU芯片上的代码。
1995年5月Sun公司推出Java Development Kit(JDK)1.0a2版本,标志着Java的诞生
Java诞生的原因
平台与机器指令
无论哪种编程语言编写的应用程序都需要经过操作系统和处理器来完成程序的运行,
因此这里所指的平台是由操作系统(OS)和处理器(CPU)所构成。
与平台无关是指软件的运行不因操作系统、处理器的变化导致发生无法运行或出现运行错误。
所谓平台的机器指令就是可以被该平台直接识别、执行的一种由0,1组成的序列代码。
C/C++程序依赖平台
C/C++程序依赖平台
Java语言相对于其他语言的最大优势就是所谓的平台无关性,即跨平台性
Java 是一门跨平台的语言,Java 是平台无关性的,
这也是Java最初风靡全球的主要原因。
这也是Java 语言可以迅速崛起并风光无限的一个重要原因。
Java 是一门跨平台的语言?Java 如何实现的平台无关性的
什么是平台无关性
平台无关性就是一种语言在计算机上的运行不受平台的约束,
一次编译,到处执行(Write Once ,Run Anywhere)。
用Java 创建的可执行二进制程序,能够不加改变的运行于多个平台。
平台无关性好处
作为一门平台无关性语言,无论是在自身发展,还是对开发者的友好度上都是很突出的。
因为其平台无关性,所以Java 程序可以运行在各种各样的设备上,尤其是一些嵌入式设备,如打印机、扫描仪、传真机等。
随着5G 时代的来临,也会有更多的终端接入网络,相信平台无关性的Java 也能做出一些贡献。
对于Java 开发者来说,Java 减少了开发和部署到多个平台的成本和时间。真正的做到一次编译,到处运行。
平台无关性的实现
对于Java 的平台无关性的支持,是分布在整个Java 体系结构中的。
就像对安全性和网络移动性的支持一样
Java 的平台无关性是建立在Java 虚拟机的平台有关性基础之上的,是因为Java 虚拟机屏蔽了底层操作系统和硬件的差异。
其中扮演者重要的角色的有
前端 编译与后端编译、Class 文件
Java虚拟机(JVM)
字节码(ByteCode)
Java 语言规范
编译原理基础?Java 到底是是如何运行起来的?
前端 编译与后端编译、Class 文件
所有Java 文件要编译成统一的Class 文件
在计算机世界中,计算机只认识0 和1,所以,真正被计算机执行的其实是由0 和1 组成的二进制文件。
日常开发使用的C、C++、Java、Python 等都属于高级语言,而非二进制语言
想要让计算机认识我们写出来的Java 代码,那就需要把他"翻译"成由0 和1 组成的二进制文件。
这个过程就叫做编译。负责这一过程的处理的工具叫做编译器。
把Java 文件,编译成二进制文件,需要经过两步编译
前端编译
主要指与源语言有关但与目标机无关的部分。
Java 中,javac的编译就是前端编译。
除了这种以外,使用的很多IDE,都内置了前端编译器。主要功能就是把.java 代码转换成.class 代码。
如eclipse,idea 等
这里提到的.class 代码,其实就是Class 文件。
后端编译
主要是将中间代码再翻译成机器语言
Java 中,这一步骤就是Java 虚拟机来执行的。
图解两步编译
图解两步编译
图解两步编译
Java 的平台无关性实现主要作用于以上阶段
Java虚拟机(JVM)
通过Java 虚拟机将Class 文件转成对应平台的二进制文件等
所谓平台无关性,就是说要能够做到可以在多个平台上都能无缝对接。
但是,对于不同的平台,硬件和操作系统肯定都是不一样的。
对于不同的硬件和操作系统,最主要的区别就是指令不同。
比如同样执行a+b,A 操作系统对应的二进制指令可能是10001000,而B 操作系统对应的指令可能是11101110。
那么,想要做到跨平台,最重要的就是可以根据对应的硬件和操作系统生成对应的二进制指令。
而这一工作,主要由Java 虚拟机完成。
虽然Java 语言是平台无关的,但JVM 却是平台有关的
不同的操作系统上面要安装对应的JVM
上图是Oracle 官网下载JDK 的指引,不同的操作系统需要下载对应的Java 虚拟机。
有了Java 虚拟机,想要执行a+b 操作,A 操作系统上面的虚拟机就会把指令翻译成10001000,B 操作系统上面的虚拟机就会把指令翻译成11101110。
所以,Java 之所以可以做到跨平台,是因为Java 虚拟机充当了桥梁。
他扮演了运行时Java 程序与其下的硬件和操作系统之间的缓冲角色。
字节码(ByteCode)
各种不同的平台的虚拟机都使用统一的程序存储格式——字节码(ByteCode)是构成平台无关性的另一个基石。
Java 虚拟机只与由字节码组成的Class 文件进行交互。
Java 语言可以Write Once ,Run Anywhere。这里的Write 其实指的就是生成Class 文件的过程。
因为Java Class 文件可以在任何平台创建,也可以被任何平台的Java 虚拟机装载并执行,所以才有了Java 的平台无关性。
Java 语言规范
通过规定Java 语言中基本数据类型的取值范围和行为
已经有了统一的Class 文件,以及可以在不同平台上将Class 文件翻译成对应的二进制文件的Java 虚拟机,Java 就可以彻底实现跨平台了吗?
其实并不是的,Java 语言在跨平台方面也是做了一些努力的,这些努力被定义Java 语言规范中。
通过保证基本数据类型在所有平台的一致性,Java 语言为平台无关性提供强了有力的支持。
Java
Java 中基本数据类型的值域和行为都是由其自己定义的。
举一个简单的例子:对于int 类型,在Java 中,int 占4 个字节,这是固定的。
C/C++
基本数据类型是由它的占位宽度决定的,占位宽度则是由所在平台决定的。
在不同的平台中,对于同一个C++程序的编译结果会出现不同的行为。
举一个简单的例子:对于int 类型,在C++中却不是固定的了。
在16 位计算机上,int 类型的长度可能为两字节;
在32 位计算机上,可能为4 字节;
当64 位计算机流行起来后,int 类型的长度可能会达到8字节。
Java程序不依赖平台
Java可以在平台之上再提供一个Java运行环境(Java Runtime Environment,JRE),
该Java运行环境由Java虚拟机(Java Virtual Machine,JVM)、类库以及一些核心文件组成。
Java虚拟机的核心是所谓的字节码指令,即可以被Java虚拟机直接识别、执行的一种由0,1组成的序列代码。
Java语言提供的编译器不针对特定的操作系统和CPU芯片进行编译,而是针对Java虚拟机把Java源程序编译为称作字节码的一种“中间代码”
Java虚拟机负责将字节码翻译成虚拟机所在平台的机器码,并让当前平台运行该机器码
如图所示
Java生成的字节码文件不依赖平台
语言无关性
其实,Java 的无关性不仅仅体现在平台无关性上面,向外扩展一下,Java 还具有语言无关性。
为了让Java 语言具有良好的跨平台能力,Java 独具匠心的提供了一种可以在所有平台上都能使用的一种中间代码——字节码(ByteCode)
有了字节码,无论是哪种平台(如Windows、Linux 等),只要安装了虚拟机,都可以直接运行字节码。
同样,有了字节码,也解除了Java 虚拟机和Java 语言之间的耦合。
这话可能很多人不理解,Java 虚拟机不就是运行Java 语言的么?这种解耦指的是什么?
JVM 其实并不是和Java 文件进行交互的,而是和Class 文件,也就是说,其实JVM 运行时,并不依赖于Java 语言。
时至今日,商业机构和开源机构,已经在Java 语言之外,发展出一大批可以在JVM 上运行的语言了
其实,目前Java 虚拟机已经可以支持很多除Java 语言以外的语言了,JVM 还支持哪些语言
Kotlin
Kotlin 是一种在Java 虚拟机上运行的静态类型编程语言,它也可以被编译成为JavaScript 源代码。
Kotlin 的设计初衷就是用来生产高性能要求的程序的,所以运行起来和Java 也是不相上下。
Kotlin 可以从JetBrains InteilliJ Idea IDE 这个开发工具以插件形式使用。
Hello World In Kotlin
Hello World In Kotlin
Groovy
Apache 的Groovy 是Java 平台上设计的面向对象编程语言。
使用Groovy 的一个重要特点就是使用类型推断
即能够让编译器能够在程序员没有明确说明的时候推断出变量的类型。
Groovy 可以使用其他Java 语言编写的库。
Groovy的语法与Java 非常相似,
实际上,Groovy 编译器是可以接受完全纯粹的Java 语法格式的。
它的语法风格与Java很像,Java 程序员能够很快的熟练使用Groovy,
大多数Java 代码也匹配Groovy 的语法规则,尽管可能语义不同。
Hello World In Groovy
Hello World In Groovy
Scala
Scala 是一门多范式的编程语言,
设计初衷是要集成面向对象编程和函数式编程的各种特性。
Scala 经常被我们描述为多模式的编程语言,因为它混合了来自很多编程语言的元素的特征。
但无论如何它本质上还是一个纯粹的面向对象语言。
它相比传统编程语言最大的优势就是提供了很好并行编程基础框架措施了。
Scala 代码能很好的被优化成字节码,运行起来和原生Java 一样快。
Hello World In Scala
Hello World In Scala
JRuby
JRuby 是用来桥接Java 与Ruby 的,它是使用比Groovy 更加简短的语法来编写代码,能够让每行代码执行更多的任务。
就和Ruby 一样,JRuby 不仅仅只提供了高级的语法格式。
它同样提供了纯粹的面向对象的实现,闭包等等,而且JRuby 跟Ruby 自身相比多了很多基于Java 类库可以调用,
虽然Ruby 也有很多类库,但是在数量以及广泛性上是无法跟Java 标准类库相比的。
Hello World In Jruby
Hello World In Jruby
Jython
Jython,是一个用Java 语言写的Python 解释器。
Jython 能够用Python 语言来高效生成动态编译的Java 字节码。
Hello World In Jython
Hello World In Jython
Fantom
Fantom 是一种通用的面向对象编程语言,
由Brian 和Andy Frank 创建,运行在三个平台上
.NET Common Language Runtime
JavaScript
Java Runtime Environment(JRE)
其主要设计目标是提供标准库API,以抽象出代码是否最终将在JRE 或CLR 上运行的问题。
Fantom 是与Groovy 以及JRuby 差不多的一样面向对象的编程语言,
但是悲剧的是Fantom 无法使用Java 类库,而是使用它自己扩展的类库。
Hello World In Fantom
Hello World In Fantom
Clojure
Clojure 是Lisp 编程语言在Java 平台上的现代、函数式及动态方言。
与其他Lisp一样,Clojure 视代码为数据且拥有一套Lisp 宏系统。
虽然Clojure 也能被直接编译成Java 字节码,但是无法使用动态语言特性以及直接调用Java 类库。
与其他的JVM 脚本语言不一样,Clojure 并不算是面向对象的。
Hello World In Clojure
Hello World In Clojure
Rhino
Rhino 是一个完全以Java 编写的JavaScript 引擎,目前由Mozilla 基金会所管理。
Rhino 的特点是为JavaScript 加了个壳,然后嵌入到Java 中,这样能够让Java 程序员直接使用。
其中Rhino 的JavaAdapters 能够让JavaScript 通过调用Java 的类来实现特定的功能。
Hello World In Rhino
Hello World In Rhino
Ceylon
Ceylon 是一种面向对象,强烈静态类型的编程语言,强调不变性,由Red Hat 创建。
Ceylon 程序在Java 虚拟机上运行, 可以编译为JavaScript。
语言设计侧重于源代码可读性,可预测性,可扩展性,模块性和元编程性。
Hello World In Ceylon
Hello World In Ceylon
之所以可以支持,就是因为这些语言也可以被编译成字节码(Class 文件)。而虚拟机并不关心字节码是有哪种语言编译而来的。
经常使用IDE 的开发者可能会发现,当在Intelij IDEA 中,鼠标右键想要创建Java 类的时候,IDE 还会提示创建其他类型的文件,
IDE 默认支持的一些可以运行在JVM 上面的语言
这就是IDE 默认支持的一些可以运行在JVM 上面的语言,没有提示的,可以通过插件来支持。
目前,可以直接在JVM 上运行的语言有很多,每种语言通过一段『HelloWorld』代码进行演示,看看不同语言的语法有何不同。
Java的地位
网络地位
Java的平台无关性让Java成为编写网络应用程序的佼佼者,
而且Java也提供了许多以网络应用为核心的技术,
使得Java特别适合于网络应用软件的设计与开发。
语言地位
Java是面向对象编程,并涉及到网络、多线程等重要的基础知识,是一门很好的面向对象语言。
Java语言不仅是一门正在被广泛使用的编程语言,而且已成为软件设计开发者应当掌握的一门基础语言
需求地位
由于很多新的技术领域都涉及到了Java语言 ,导致IT行业对Java人才的需求正在不断的增长 。
Java的特点
一门【面向对象】的编程语言
什么是面向对象?
面向对象(Object Oriented)是一种软件开发思想。
它是对现实世界的一种抽象,
面向对象会把相关的数据和方法组织为一个整体来看待
相对的另外一种开发思想就是面向过程的开发思想
什么面向过程?
面向过程(Procedure Oriented)是一种以过程为中心的编程思想。
举个例子
比如你是个学生,你每天去上学需要做几件 事情?
起床、穿衣服、洗脸刷牙,吃饭,去学校。一般是顺序性的完成一系列动作。
面向过程
而面向对象可以把学生进行抽象,所以这个例子就会变为
面向对象
面向过程
可以不用严格按照顺序来执行每个动作
不用手动管理对象的生命周期
Java摒弃了 C++中难以理解的概念;
多继承
指针
内存管理
不用手动管理对象的生命周期
功能强大和【简单】易用
现在企业级开发,快速敏捷开发
尤其是各种框架 的出现,使Java成为越来越火的一门语言。
一门【静态语言】,执行效率要比动态语言高,速度更快
从设计的角度上来说,所有的语言都是设计用来把人类可读的代码转换为机器指令
静态语言
静态语言是什么?
在编译期间就能够知道数据类型的语言
在运行前就能 够检查类型的正确性,一旦类型确定后就不能再更改
比如
举例
静态语言设计是用来让硬件执行的 更高效,因此需要程序员编写准确无误的代码,以此来让代码尽快的执行。
从这个角度来说,静态语言的执行效率要比动态语言高,速度更快
静态语言主要有
Pascal, Perl, C/C++, JAVA, C#, Scala 等。
动态语言
动态语言是什么?
动态语言没有任何特定的情况需要指定变量的类型
在运行时确定的数据类型
为了能够让程序员提高编码效率,因此使用更少的代码来实现功能
动态语言主要有
Lisp, Perl, Python、Ruby、JavaScript 等
易实现【多线程】
Java是一门高级语言,高级语言会对用户屏蔽很多底层实现细节。
比如Java是如何实现多线程的。
从 操作系统的角度来说,实现多线程的方式主要有下面这几种
在用户空间中实现多线程
Java应该是在用户空间实现的多线程
在内核空间中实现多线程
内核是感知不到Java存在多线程机制的
在用户和内核空间中混合实现线程
具有平台独立性和可移植性
Java有一句非常著名的口号:Write once, run anywhere,也就是一次编写、到处运行。
为什么 Java能够吹出这种牛批的口号来?核心就是JVM。
计算机应用程序和硬件之间会屏蔽很多 细节,它们之间依靠操作系统完成调度和协调,
体系结构如下,操作系统所处的位置
操作系统所处的位置
那么加上Java应用、JVM的体系结构会变为如下
JVM的体系结构
Java是跨平台的,已编译的Java程序可以在任何带有JVM的平台上运行。
在Windows平台 下编写代码,然后拿到Linux平台下运行,该如何实现呢?
首先,需要在应用中编写Java代码;
用Eclipse或者javac,把Java代码编译为.class文件;
然后把.class文件打成.jar文件;
然后.jar文件就能够在Windows、Mac OS X、Linux系统下运行了。
不同的操作系统有不同的 JVM实现,切换平台时,不需要再次编译Java代码了。
具有高性能
编写的代码,经过javac编译器编译称为 字节码(bytecode),经过JVM内嵌的解释器将字节码 转换为机器代码,这是解释执行,这种转换过程效率较低。
但是部分JVM的实现都提供了 JIT(Just-In-Time)编译器
比如Hotspot JVM
JIT编译器
是什么?
动态编译器
JIT(Just-In-Time)编译器
能做什么?
在运行时,将热点代码编译成机器码
优点
运行效率高
编译执行
所以Java不仅仅只是一种解释执行的语言。
具有健壮性
重要保证
强类型机制
异常处理
垃圾的自动收集
这也是Java与C 语言的重要区别
容易开发分布式项目
支持互联网应用的开发
RMI
Java的RMI机制也是开发分布式应用的重要手段。
远程方法调用
网络应用编程类库
有net api,
它提供了用于网络应用编程的类库
URL
URLConnection、
Socket
ServerSocket
安全
Java开发环境
Oracle提供了两种Java平台的实现,JDK和JRE的区别?
JDK
JDK (Java Development Kit)称为Java开发包或Java开发工具
Java开发标准工具包
是一个编写Java的Applet 小程序和应用程序的程序开发环境。
JDK是Java开发工具包
JDK的功能要比JRE全很多。
JDK是整个Java的核心,包括了 Java运行环境(Java Run time Environment),—些 Java 工具和 Java 的核心类库(Java API)
Java8 JDK的全貌
Java8 JDK的全貌
可以认真研究一下这张图,它几乎包括了 Java中所有的概念
使用的是jdk1.8,可以点进去 Description of Java Conceptual Diagram ,可以发现这里面包括了所有关于Java的描述
JRE
Java Runtime Environment, Java运行时环境。
JRE是个运行环境,JDK是个开发环境。
JRE是Java运行环境,JDK中包含了JRE。
写Java程序时需要JDK,而运行Java程序时 就需要JRE。
JDK里面已经包含了JRE,因此只要安装了JDK,就可以编辑Java程序,也可以正常 运行Java程序。
由于JDK包含了许多与运行无关的内容,占用的空间较大,因此运行普通的Java 程序无须安装JDK,而只需要安装JRE即可。
Java开发环境配置
安装Java SE平台
Java SE平台是学习掌握Java语言的最佳平台,而掌握Java SE又是进一步学习Java EE和Java ME所必须的
1 下载JDK1.8。 本书将使用针对Window操作系统平台的JDK,因此下载的版本为jjdk-8u40-windows-i586.exe。
2 选择安装路径界面。为了便于今后设置环境变量,建议修改默认的安装路径为:D:\jdk1.8 。
3 系统环境path的设置。
4 系统环境classpath的设置。
2 选择安装路径界面。为了便于今后设置环境变量,建议修改默认的安装路径为:D:\jdk1.8 。
3 系统环境path的设置。
4 系统环境classpath的设置。
子主题
Java程序的开发步骤
1.编写源文件。扩展名必须是.java。
2.编译Java源程序。使用Java编译器(javac.exe)编译源文件,得到字节码文件。
3. 运行Java程序。使用Java SE平台中的Java解释器(java.exe)来解释执行字节码文件。
2.编译Java源程序。使用Java编译器(javac.exe)编译源文件,得到字节码文件。
3. 运行Java程序。使用Java SE平台中的Java解释器(java.exe)来解释执行字节码文件。
Java程序的开发过程
Java程序的开发过程
一个简单的Java应用程序
Java是面向对象编程,Java应用程序可以由若干个Java源文件所构成,每个源文件又是由若干个书写形式互相独立的类组成,
但其中一个源文件必须有一个类包含有main方法,该类称做应用程序的主类。Java应用程序从主类的main方法开始执行。
编写源文件
public class Hello {
public static void main (String args[]) {
System.out.println("这是一个简单的Java应用程序");
}
}
public static void main (String args[]) {
System.out.println("这是一个简单的Java应用程序");
}
}
注
1.应用程序的主类是Hello
2.源文件的命名 :Hello.java
1.应用程序的主类是Hello
2.源文件的命名 :Hello.java
编译
当保存了Hello.java源文件后,就要使用Java编译器(javac.exe)对其进行编译。
如果源文件没有错误,编译源文件将生成扩展名为.class的字节码文件,其文件名与该类的名字相同,被存放在与源文件相同的目录中。
子主题
编译例1-1中Hello.java源文件将得到Hello.class。
如果对源文件进行了修改,必须重新编译,再生成新的字节码文件。
如果编译出现错误提示,必须修改源文件,然后再进行编译。
运行
使用Java虚拟机中的Java解释器(java.exe)来解释执行其字节码文件。
Java应用程序总是从主类的main方法开始执行。
因此,需进入主类字节码所在目录,如C:\chapter1,然后使用Java解释器(java.exe)运行主类的字节码
使用Java虚拟机中的Java解释器(java.exe)来解释执行其字节码文件。
JDK1.8(8.0)环境变量配置
下载JDK
到Oracle官网下载http://www.oracle.com/technetwork/java/javase/downloads/index.html
子主题
子主题
子主题
找到环境变量
右键我的电脑——属性——高级系统设置——环境变量
设置path变量
找到path变量——编辑——变量值:JDK安装目录(默认在C:\Program Files\Java\jdk1.8.0_40\bin)
如果变量值已有其他值,请用";"分隔
找到path变量——编辑——变量值
配置CLASSPATH
变量值:.;C:\Program Files\Java\jdk1.8.0_40\lib\tools.jar;C:\Program Files\Java\jdk1.8.0_40\lib\dt.jar(如果你的jdk不是jdk1.8.0_40版本的,请修改)
配置CLASSPATH
配置JAVA_HOME
新建变量:JAVA_HOME
变量值:JDK安装目录
变量值:JDK安装目录
新建变量:JAVA_HOME。变量值:JDK安装目录
检验完成
配置完成,检查一下有没有安装好,打开命令行,输入java -version,显示jdk版本信息如下图
配置完成,检查一下有没有安装好
(关键)配置Java环境
1、输入“JAVA_HOME”
1、输入“JAVA_HOME”
弹出“新建系统变量”对话框,在“变量名”文本框输入“JAVA_HOME”,
在“变量值”文本框输入JDK的安装路径(也就是步骤5的文件夹路径),单击“确定”按钮
在“变量值”文本框输入JDK的安装路径(也就是步骤5的文件夹路径),单击“确定”按钮
2、新建变量 PATH
2、 在“系统变量”选项区域中查看PATH变量,如果不存在,则新建变量 PATH,否则选中该变量,
单击“编辑”按钮,在“变量值”文本框的起始位置添加“%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;”
或者是直接“%JAVA_HOME%\bin;”,单击确定按钮
单击“编辑”按钮,在“变量值”文本框的起始位置添加“%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;”
或者是直接“%JAVA_HOME%\bin;”,单击确定按钮
2、新建变量 PATH
3、新建变量CLASSPATH
3、在“系统变量”选项区域中查看CLASSPATH 变量,如果不存在,则新建变量CLASSPATH,否则选中该变量
单击“编辑”按钮,在“变量值”文本框的起始位置添加“.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;”。
单击“编辑”按钮,在“变量值”文本框的起始位置添加“.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;”。
子主题
在配置完Java开发环境,并下载Java开发工具(Eclipse、IDEA等)后,就可以.写Java代码了
Java平台主要分为下列3个版本
Java要实现“编写一次,到处运行”(write once,run anywhere)的目标,就必须提供相应的Java运行环境,即运行Java程序的平台。
目前Java平台主要分为下列3个版本
(1)Java SE(曾称为J2SE)称为Java标准版或Java 标准平台。
(2)Java EE(曾称为J2EE)称为Java企业版或Java企业平台。
(3)Java ME(曾称为J2ME)称为Java微型版或Java小型平台。
虚拟机负责将字节码文件加载到内存,然后采用解释方式来执行字节码文件,即根据相应平台的机器指令翻译一句执行一句。
字节码文件(包括程序使用的类库中的字节码)
Java编程基础概念
Java基本数据类型与基本类型的类包装
基本数据类型
简单数据类型也称作基本数据类型
基本数据类型
基本类型,或者叫做内置类型,是Java 中不同于类(Class)的特殊类型。
它们是编程中使用最频繁的类型。
Java 是一种强类型语言,第一次申明变量必须说明数据类型,第一次变量赋值称为变量的初始化。
Java的基本数据类型包括
Java 基本类型共有八种,基本类型可以分为三类:
字符类型char
布尔类型boolean
数值类型byte、short、int、long、float、double。
数值类型又可以分为整数类型byte、short、int、long 和浮点数类型float、double。
布尔类型boolean
数值类型byte、short、int、long、float、double。
数值类型又可以分为整数类型byte、short、int、long 和浮点数类型float、double。
Java语言有8种基本数据类型
byte、int、short、long、float、double、char。
Java有哪8种基本数据类型?Java支持的数据类型有哪些?Java语言支持的8中基本数据类型+1
int
long
double
float
byte
boolean
char
short
图解数据类型
图解数据类型
以上x位都指的是在内存中的占用。
实际上, Java中还存在另外一种基本类型void, 它也有对应的包装类java.lang.Void,不过我们无法直接对它们进行操作。
还有个void,别忘了
常见问题
基本数据类型有什么好处
在Java 语言中,new 一个对象是存储在堆里的,通过栈中的引用来使用这些对象;所以,对象本身来说是比较消耗资源的。
对于经常用到的类型,如int 等,如果每次使用这种变量时,都需要new 一个Java 对象,就会比较笨重。
所以,和C++一样,Java 提供了基本数据类型
这种数据的变量不需要使用new 创建
不会在堆上创建,而是直接在栈内存中存储,因此会更加高效。
超出表示范围的溢出问题?超出范围怎么办
整型的取值范围
Java 中的整型主要包含byte、short、int 和long 这四种,
表示的数字范围也是从小到大的,之所以表示范围不同主要和他们存储数据时所占的字节数有关。
整型中,每个类型都有一定的表示范围,但是,在程序中有些计算会导致超出表示范围,即溢出
溢出如以下代码
int i = Integer.MAX_VALUE;
int j = Integer.MAX_VALUE;
int k = i + j;
System.out.println("i (" + i + ") + j (" + j + ") = k (" + k + ")");
int j = Integer.MAX_VALUE;
int k = i + j;
System.out.println("i (" + i + ") + j (" + j + ") = k (" + k + ")");
输出结果:i (2147483647) + j (2147483647) = k (-2)
这就是发生了溢出,溢出的时候并不会抛异常,也没有任何提示。
所以,在程序中,使用同类型的数据进行运算时,一定要注意数据溢出的问题。
Java中数据类型只有四类八种
简答科普
1 字节=8 位(bit)
8bit 可以表示的数字
最小值:10000000 (-128)(-2^7)
最大值:01111111(127)(2^7-1)
最大值:01111111(127)(2^7-1)
整数型
byte
字节
byte 用1 个字节来存储
科普:1 字节=8 位(bit)
1 byte = 8 bits,
先来看计算中8bit 可以表示的数字:
最小值:10000000 (-128)(-2^7)
最大值:01111111(127)(2^7-1)
Java 中,为什么byte 类型的取值范围为-128~127?
范围为-128(-2^7)到127(2^7-1),
默认值是0
在变量初始化时,byte 类型的默认值为0。
常量
Java中不存在byte型常量的表示法,但可以把一定范围内的int型常量赋值给byte型变量。
变量
使用关键字byte来声明byte 型变量
例如: byte x= -12,tom=28,漂亮=98;
对于byte型内存分配给1个字节,占8位 。
short
占用两个字节,也就是16位,
用2 个字节存储
1 short =2byte= 16 bits,
默认值是0
在变量初始化时,short 类型的默认值为0
范围为-32,768 (-2^15)到32,767 (2^15-1)
一般情况下,因为Java 本身转型的原因,可以直接写为0。
常量
和byte型类似,Java中也不存在short型常量的表示法,
但可以把一定 范围内的int型常量赋值给short型变量。
变量
使用关键字short来声明short型变量
例如: short x=12,y=1234;例如: short x=12,y=1234;
对于short型变量,内存分配给2个字节,占16位.
int
占用四个字节,也就是32位,
int 用4 个字节存储
1 int = 4byte=32 bits
范围为-2,147,483,648 (-2^31)到2,147,483,647 (2^31-1)
默认值是0
在变量初始化时,int 类型的默认值为0。
常量
123,6000(十进制),077(八进制),0x3ABC(十六进制)
变量
使用关键字int来声明int型变量,声明时也可以赋给初值
例如: int x= 12,平均=9898,jiafei;
对于int型变量,内存分配给4个字节(byte),-231~231-1
long
占用八个字节,也就是64位
用8 个字节存储
1 long =8byte= 64 bits
范围为-9,223,372,036,854,775,808 (-2^63)到9,223,372,036, 854,775,807 (2^63-1)
默认值是0L
在变量初始化时,long 类型的默认值为0L 或0l,也可直接写为0。
常量
long型常量用后缀L来表示,
例如:108L(十进制)、07123L(八进制)、0x3ABCL(十六进制) 。
变量
使用关键字long来声明long型变量,
例如: long width=12L,height=2005L,length;
对于long型变量,内存分配给8个字节,占64位。
java 中的整型属于有符号数。
整型中byte、short、int、long 的取值范围
整数型的占用字节大小空间为long > int > short > byte
Java 中的整型主要包含byte、short、int 和long 这四种,表示的数字范围也是从小到大的
之所以表示范围不同,主要和他们存储数据时,所占的字节数有关。
浮点型
什么是浮点型?
在计算机科学中,浮点是一种对于实数的近似值数值表现法,由一个有效数字(即尾数)加上幂数来表示,
通常是乘以某个基数的整数次指数得到。以这种表示法表示的数值,称为浮点数(floating-point number)。
计算机使用浮点数运算的主因
计算机使用浮点数运算的主因,在于电脑使用二进位制的运算。
例如:4÷2=2,4 的二进制表示为100、2 的二进制表示为010,在二进制中,相当于退一位数(100 -> 010)。
1 的二进制是01,1.0/2=0.5,那么,0.5 的二进制表示应该为(0.1),以此类推,0.25的二进制表示为0.01,
并不是说所有的十进制小数都能准确的用二进制表示出来
所以,并不是说所有的十进制小数都能准确的用二进制表示出来,如0.1,因此只能使用近似值的方式表达。
也就是说,十进制的小数在计算机中是由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到的,
这种表示方法类似于基数为10 的科学计数法。
一个浮点数a 由两个数m 和e 来表示
a = m × be。
在任意一个这样的系统中,选择一个基数b(记数系统的基)和精度p(即使用多少位来存储)。
m(即尾数)是形如±d.ddd...ddd 的p 位数(每一位是一个介于0 到b-1 之间的整数,包括0 和b-1)。
如果m 的第一位是非0 整数,m 称作正规化的。
有一些描述使用一个单独的符号位(s 代表+或者-)来表示正负,这样m 必须是正的。e 是指数。
位(bit)的概念 以及 什么是单精度和双精度?
位(bit)是衡量浮点数所需存储空间的单位,
通常为32 位或64 位,分别被叫作单精度和双精度。
什么是单精度和双精度?
单精度浮点数在计算机存储器中占用4 个字节(32 bits),利用“浮点”(浮动小数
点)的方法,可以表示一个范围很大的数值。
点)的方法,可以表示一个范围很大的数值。
在计算机存储器中占用4 个字节(32 bits),
利用“浮点”(浮动小数点)的方法,可以表示一个范围很大的数值。
双精度浮点数
双精度浮点数(double)使用64 位(8 字节) 来存储一个浮点数。
为什么不能用浮点型表示金额?
由于计算机中保存的小数其实是十进制的小数的近似值,并不是准确值,
所以,千万不要在代码中使用浮点数来表示金额等重要的指标。
建议使用BigDecimal 或者Long(单位为分)来表示金额。
float
单精度浮点型
占用4位
1 float = 32 bits
默认值是0.0f
常量
需要特别注意的是:常量后面必须要有后缀“f”或“F”。
453.5439f,21379.987F,231.0f(小数表示法),2e40f(2乘10的40次方,指数表示法)
变量
使用关键字float来声明float型变量,
例如:float x=22.76f,tom=1234.987f,weight=1e-12F;
对于float型变量,内存分配给4个字节,占32位。
精度:float变量在存储float型数据时保留8位有效数字,实际精度取决于具体数值。
double
双精度浮点型
占用8位
1 double = 64 bits
默认值是0.0d
常量
2389.539d,2318908.987,0.05(小数表示法),1e-90(1乘10的-90次方,指数表示法)。
对于double常量,后面可以有后缀“d”或“D”,但允许省略该后缀。
变量
使用关键字double来声明double型变量,
例如:double height=23.345,width=34.56D,length=1e12;
对于double型变量,内存分配给8个字节,占64位 。
精度:double变量在存储double型数据时保留16位有效数字,实际精度取决于具体数值。
字符型
char
char类型是一个单一的16位Unicode字符
最小值是\u0000 (也就是0 )
最大值是\uffff (即为65535)
可存储任何字符,例如char a = 'A'
常量
常量:‘A’,‘b’,‘?’,‘!’,‘9’,‘好’,‘\t’,‘き’,‘モ’等,
即用单引号扩起的Unicode表中的一个字符。
变量
使用关键字char来声明char型变量,
例如:char ch=‘A’,home=‘家’,handsome=‘酷’;
对于char型变量,内存分配给2个字节,占16位
转意字符常量
有些字符(如回车符)不能通过键盘输入到字符串或程序中,就需要使用转意字符常量,
例如:\n(换行),\b(退格),\t(水平制表), \‘(单引号),\“(双引号),\\(反斜线)等。
要观察一个字符在Unicode表中的顺序位置,可以使用int型显示转换,如(int)'a'或int p='a'。
如果要得到一个0~65536之间的数所代表的Unicode表中相应位置上的字符,必须使用char型显示转换
分别用显示转换来显示一些字符在Unicode表中的位置,以及Unicode表中某些位置上的字符
public class example {
public static void main (String args[ ]) {
char ch1='国',ch2='庆';
int p1=969,p2=12353;
System.out.println(ch1+"在Unicode表中的位置:"+(int)ch1);
System.out.println(ch2+"在Unicode表中的位置:"+(int)ch2);
System.out.println("第"+p1+"个位置上的字符是:"+(char)p1);
System.out.println("第"+p2+"个位置上的字符是:"+(char)p2);
}
}
public static void main (String args[ ]) {
char ch1='国',ch2='庆';
int p1=969,p2=12353;
System.out.println(ch1+"在Unicode表中的位置:"+(int)ch1);
System.out.println(ch2+"在Unicode表中的位置:"+(int)ch2);
System.out.println("第"+p1+"个位置上的字符是:"+(char)p1);
System.out.println("第"+p2+"个位置上的字符是:"+(char)p2);
}
}
输出
布尔型/逻辑类型
boolean
常量
true或者是false
变量
使用关键字boolean来声明逻辑变量, 声明时也可以赋给初值,
例如:boolean x,ok=true,关闭=false;
boolean只有两种值
只表示1位
默认值是false
简单数据类型的级别与类型转换运算
Java中数据的基本类型(不包括逻辑类型)按精度从“低”到“高”排列:byte short char int long float double
当把级别低的变量的值赋给级别高的变量时,系统自动完成数据类型的转换。
例如: float x=100;
当把级别高的变量的值赋给级别低的变量时,必须使用显示类型转换运算。
显示转换的格式:(类型名)要转换的值;
例如: int x=(int)34.89;
当把一个int型常量赋值给一个byte和short型变量时,不可以超出这些变量的取值范围,否则必须进行类型转换运算;
例如,常量128的属于int型常量,超出byte变量的取值范围,
如果赋值给byte型变量,必须进行byte类型转换运算(将导致精度的损失),
如所示: byte a=(byte)128;
基本类型的类包装、自动装箱与拆箱
为什么需要包装类 ?
很多人会有疑问,既然Java 中为了提高效率,提供了八种基本数据类型,为什么还要提供包装类呢?
这个问题,其实前面已经有了答案
Java 语言是一个面向对象的语言,,很多地方都需要使用对象而不是基本数据类型
但是Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,
比如,在集合类中,无法将int 、double 等类型放进去的。因为集合的容器要求元素是Object 类型。
为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,
为了让基本类型也具有对象的特征,就出现了包装类型
这些包装类分别是:
Byte
构造方法 :Byte(byte num)
调用byteValue()方法返回该对象含有的基本型数据
Integer
构造方法 : Integer(int num)
调用intValue()方法返回该对象含有的基本型数据
Short
构造方法: Short(short num)
调用shortValue()方法返回该对象含有的基本型数据
Long
构造方法: Long(long num)
调用longValue ()方法返回该对象含有的基本型数据
Float类
实现了对float基本型数据的类包装
构造方法: Float(float num)
Float对象调用floatValue()方法可以返回该对象含有的float型数据。
Double类
实现了对double基本型数据的类包装
构造方法:Double(double num)
Double对象调用doubleValue()方法可以返回该对象含有的double型数据。
Character类
实现了对char基本型数据的类包装
构造方法:Character(char c)
常用类方法:
public static boolean isDigit(char ch) 如果ch是数字字符方法返回 true,否则返回false。
public static boolean isLetter(char ch) 如果ch是字母方法返回 true,否则返回false。
public static boolean isLetterOrDigit(char ch) 如果ch是数字字符或字母方法返回 true,否则返回false。
public static boolean isLowerCase(char ch) 如果ch是小写字母方法返回 true,否则返回false。
public static boolean isUpperCase(char ch) 如果ch是大写字母方法返回 true,否则返回false。
public static char toLowerCase(char ch) 返回ch的小写形式。
public static char toUpperCase(char ch) 返回ch的大写形式。
public static boolean isSpaceChar(char ch) 如果ch是空格返回true。
public static boolean isLetter(char ch) 如果ch是字母方法返回 true,否则返回false。
public static boolean isLetterOrDigit(char ch) 如果ch是数字字符或字母方法返回 true,否则返回false。
public static boolean isLowerCase(char ch) 如果ch是小写字母方法返回 true,否则返回false。
public static boolean isUpperCase(char ch) 如果ch是大写字母方法返回 true,否则返回false。
public static char toLowerCase(char ch) 返回ch的小写形式。
public static char toUpperCase(char ch) 返回ch的大写形式。
public static boolean isSpaceChar(char ch) 如果ch是空格返回true。
包装类和基本数据类型的对应关系如下表所示
包装类和基本数据类型的对应关系所示
在这八个类名中,除了Integer 和Character 类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可。
这样八个和基本数据类型对应的类。统称为包装类(Wrapper Class)。
它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
什么是自动拆装箱?自动装箱与拆箱
JDK1.5新增的功能,JDK1.5后可以使用
(Autoboxing and Auto-Unboxing of Primitive Types)。
实现功能
在Java SE5 中,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能。
Java 自动将原始类型值转换成对应的对象
基本类型数据和相应的对象之间相互自动转换
Java提供了基本数据类型相关的类,实现了对基本数据类型的封装。
Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化。
为什么叫自动装箱,自动 拆箱?
因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱
原始类型byte, short, char, int, long, float, double 和boolean
对应的封装类为Byte,Short, Character, Integer, Long, Float, Double, Boolean。
包装类型 、包装类
存放位置
包装类均位于java.lang 包
这些类在java.lang包中。
拆箱与装箱
那么,有了基本数据类型和包装类,肯定有些时候要在他们之间进行转换。
比如把一个基本数据类型的int 转换成一个包装类型的Integer 对象。
我们认为包装类是对基本类型的包装,所以,把基本数据类型转换成包装类的过程就是
打包装,英文对应于boxing,中文翻译为装箱。
打包装,英文对应于boxing,中文翻译为装箱。
反之,把包装类转换成基本数据类型的过程就是拆包装,英文对应于unboxing,中文翻译为拆箱。
在Java SE5 之前,要进行装箱,可以通过以下代码:
Integer i = new Integer(10);
自动拆箱与自动装箱
在Java SE5 中,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能。
自动装箱
程序允许把一个基本数据类型添加到类似链表等数据结构中,系统会自动完成基本类型到相应对象的转换(自动装箱)。
比如:把int转化成Integer,double转化成Double,等等。
值类型转化为引用类型
比如将int 的变量转换成Integer 对象,这个过程叫做装箱
反之将Integer 对象转换成int 类型值,这个过程叫做拆箱
将基本数据类型自动转换成对应的包装类。
Java 的编译器把基本数据类型自动转换成封装类对象的过程叫做自动装箱,相当于使用valueOf 方法
Integer i =10; //自动装箱
可替代Integer i = new Integer(10);
这就是因为Java提供了自动装箱功能,不需要开发者手动去new 一个Integer 对象。
Integer a = 10; //this is autoboxing
Integer b = Integer.valueOf(10); //under the hood
Integer b = Integer.valueOf(10); //under the hood
自动拆箱
当从一个数据结构中获取的对象时,如果该对象是基本数据的封装对象,那么系统自动完成对象到基本类型的转换(自动拆箱)
反之就是自动拆箱。
引用类型转化为值类型
将包装类自动转换成对应的基本数据类型。
Integer i =10; //自动装箱
int b= i; //自动拆箱
int b= i; //自动拆箱
自动装箱与自动拆箱的实现原理(语法糖)
既然Java 提供了自动拆装箱的能力,那么到底是什么原理,Java 是如何实现的自动拆装箱功能?
以int和Integer装箱的过程为例子说明
我们有以下自动拆装箱的代码:
public static void main(String[]args) {
Integer integer=1; //装箱
int i=integer; //拆箱
}
Integer integer=1; //装箱
int i=integer; //拆箱
}
对以上代码进行反编译后可以得到以下代码:
public static void main(String[]args) {
Integer integer=Integer.valueOf(1);
int i=integer.intValue();
}
Integer integer=Integer.valueOf(1);
int i=integer.intValue();
}
从上面反编译后的代码可以看出,
int 的自动装箱都是通过Integer.valueOf()方法来实现的,
Integer 的自动拆箱都是通过integer.intValue 来实现的。
将八种类型都反编译一遍,发现以下规律:
自动装箱都是通过包装类的valueOf()方法来实现的.
自动拆箱都是通过包装类对象的xxxValue()来实现的。
糖块三、自动装箱与拆箱
自动装箱(语法糖解析)
先来看个自动装箱的代码
先来看个自动装箱的代码
反编译后代码如下:
反编译后代码如下:
从反编译得到内容可以看出
在装箱时,自动调用的是Integer 的valueOf(int)方法
装箱过程是通过调用包装器的valueOf 方法实现的
自动拆箱(语法糖解析)
再来看个自动拆箱的代码:
再来看个自动拆箱的代码:
反编译后代码如下:
反编译后代码如下:
从反编译得到内容可以看出
在拆箱时,自动调用的是Integer 的intValue 方法。
拆箱过程是通过调用包装器的xxxValue 方法实现的。
可能遇到的坑
对象相等比较
public static void main(String[] args) {
Integer a = 1000;
Integer b = 1000;
Integer c = 100;
Integer d = 100;
System.out.println("a == b is " + (a == b));
System.out.println(("c == d is " + (c == d)));
}
Integer a = 1000;
Integer b = 1000;
Integer c = 100;
Integer d = 100;
System.out.println("a == b is " + (a == b));
System.out.println(("c == d is " + (c == d)));
}
输出结果:
a == b is false
c == d is true
c == d is true
在Java 5 中,在Integer 的操作上,引入了一个新功能,来节省内存和提高性能。
整型对象,通过使用相同的对象引用,实现了缓存和重用。
适用于整数值区间-128 至+127。
只适用于自动装箱。使用构造函数创建对象不适用。
哪些地方会自动拆装箱? 什么情况下,Java 会进行自动拆装箱
场景1:变量的初始化和赋值的场景
场景一、将基本数据类型放入集合类
Java 中的集合类只能接收对象类型,那么以下代码为什么会不报错呢?
代码为什么会不报错呢
将上面代码进行反编译,可以得到以下代码:
将上面代码进行反编译
以上,可得出结论,当把基本数据类型放入集合类中时,会进行自动装箱。
场景二、包装类型和基本类型的大小比较
当对Integer 对象与基本类型进行大小比较时,实际上比较的是什么内容呢?
看以下代码:
看以下代码:
对以上代码进行反编译,得到以下代码:
反编译后的代码
包装类与基本数据类型进行比较运算,是先将包装类进行拆箱成基本数据类型,然后进行比较的。
场景三、包装类型的运算(四则运算)
当对Integer 对象进行四则运算时,是如何进行的呢?
看以下代码:
实例代码
反编译后代码如下:
反编译后的代码
发现,两个包装类型之间的运算,会被自动拆箱成基本类型进行。
场景四、三目运算符的使用
这是很多人不知道的一个场景,作者也是一次线上的血淋淋的Bug 发生后,才了解到的一种案例。
看一个简单的三目运算符的代码:
很多人不知道,其实在int k = flag ? i : j;这一行,会发生自动拆箱
(JDK1.8 之前,相见:https://www.hollischuang.com/archives/4749)
反编译后代码如下:
反编译后代码
这其实是三目运算符的语法规范。当第二,第三位操作数分别为基本类型和对象时,其中的对象就会拆箱为基本类型进行操作。
因为例子中,flag ? i : j;片段中,第二段的i 是一个包装类型的对象,而第三段的j 是一个基本类型,所以会对包装类进行自动拆箱。
如果这个时候i 的值为null,那么就会发生NPE。(自动拆箱导致空指针异常)
场景五、函数参数与返回值
这个比较容易理解,直接上代码了:
子主题
自动拆装箱与缓存
Java SE 的自动拆装箱还提供了一个和缓存有关的功能,
先来看以下代码,猜测一下输出结果:
看以下代码,猜测一下输出结果
普遍认为上面的两个判断的结果都是false。
虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个if 判断都是false 的。
在Java 中,==比较的是对象应用,而equals 比较的是值。
所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回false。
奇怪的是,这里两个类似的if 条件判断返回不同的布尔值。
上面这段代码真正的输出结果
integer1 == integer2
integer3 != integer4
integer3 != integer4
Java 中Integer 的缓存实现
原因就和Integer 中的缓存机制有关。
在Java 5 中,在Integer 的操作上引入了一个新功能来节省内存和提高性能。
这是在Java 5 中引入的一个有助于节省内存、提高性能的功能。
整型对象通过使用相同的对象引用实现了缓存和重用。
看看JDK 中的valueOf 方法
Java 的编译器把基本数据类型自动转换成封装类对象的过程叫做自动装箱,相当于使用valueOf 方法
JDK 1.8.0 build 25 的实现
在创建对象之前先从IntegerCache.cache 中寻找。如果没找到才使用new 新建对象。
IntegerCache,Integer 类中的内部类
IntegerCache 是Integer 类中定义的一个private static 的内部类。
接下来看看他的定义。
子主题
子主题
其中的javadoc 详细的说明了缓存支持-128 到127 之间的自动装箱过程。
适用于整数值区间-128 至+127。
只适用于自动装箱。使用构造函数创建对象不适用
缓存支持-128 到127 之间的自动装箱过程
当需要进行自动装箱时,如果数字在-128 至127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象。
其中的javadoc 详细的说明了缓存支持-128 到127 之间的自动装箱过程。
实际上这个功能在Java 5 中引入的时候,范围是固定的-128 至+127。
这使我们可以根据应用程序的实际情况灵活地调整来提高性能。
Java 5 中引入时,范围是固定的-128 至+127,最大值127可以通过-XX:AutoBoxCacheMax=size 修改。
缓存通过一个for 循环实现。从低到高并创建尽可能多的整数并存储在一个整数数组中。
这个缓存会在Integer 类第一次被使用的时候被初始化出来。
以后,就可以使用缓存中包含的实例对象,而不是创建一个新的实例(在自动装箱的情况下)。
后来在Java6 中,可以通过java.lang.Integer.IntegerCache.high 设置最大值。
到底是什么原因选择这个-128 到127 范围呢?
因为这个范围的数字是最被广泛使用的。
在程序中,第一次使用Integer 时,也需要一定的额外时间来初始化这个缓存。
Java 语言规范中的缓存行为
在Boxing Conversion 部分的Java 语言规范(JLS)规定如下:
如果一个变量p 的值是:
变量p 的值
范围内的时,将p 包装成a 和b 两个对象时,可以直接使用a==b 判断a 和b 的值是否相等。
其他缓存的对象
这种缓存行为不仅适用于Integer 对象。针对所有的整数类型的类都有类似的缓存机制。
有ByteCache 用于缓存Byte 对象
有ShortCache 用于缓存Short 对象
有LongCache 用于缓存Long 对象
有CharacterCache 用于缓存Character 对象
有ShortCache 用于缓存Short 对象
有LongCache 用于缓存Long 对象
有CharacterCache 用于缓存Character 对象
Byte, Short, Long 有固定范围: -128 到127。
对于Character, 范围是0 到127。
除了Integer 以外,这个范围都不能改变。
自动拆装箱带来的问题
自动拆装箱是一个很好用的功能,大大节省了开发人员的精力,不再需要关心到底什么时候需要拆装箱。
但是,它也会引入一些问题。
包装对象的数值比较,不能简单的使用==,虽然-128 到127 之间的数字可以,但是这个范围之外还是需要使用equals 比较。
有些场景会进行自动拆装箱,同时由于自动拆箱,如果包装类对象为null,那么自动拆箱时就有可能抛出NPE。
如果一个for 循环中有大量拆装箱操作,会浪费很多资源。
如何正确定义接口的返回值(boolean/Boolean)类型及命名(success/isSuccess)
该如何定一个布尔类型的成员变量 ?
关于这个"本次请求是否成功"的字段定义,其实是有很多种讲究和坑的,稍有不慎就会掉入坑里,
在日常开发中,经常要在类中定义布尔类型的变量,
比如在给外部系统提供一个RPC 接口时,一般会定义一个字段表示本次请求是否成功的。
到底哪一种才是正确的使用姿势呢?
一般情况下,可以有以下四种方式来定义一个布尔类型的成员变量:
Boolean success
boolean isSuccess
Boolean success
Boolean isSuccess
boolean isSuccess
Boolean success
Boolean isSuccess
以上四种定义形式,日常开发中最常用的是哪种呢?到底哪一种才是正确的使用姿势呢?
通过观察可以发现
前两种和后两种的主要区别是变量的类型不同,前者使用的是boolean,后者使用的是Boolean。
第一种和第三种在定义变量时,变量命名是success,而另外两种使用isSuccess 来命名的。
success 还是isSuccess ?
到底应该是用success 还是isSuccess 来给变量命名呢?
从语义上面来讲,两种命名方式都可以讲的通,并且也都没有歧义。
那么还有什么原则可以参考来让我们做选择呢。
在Java 开发手册中关于这一点,有过一个『强制性』规定:
那么,为什么会有这样的规定呢?看一下POJO 中布尔类型变量不同的命名有什么区别吧。
子主题
以上代码的setter/getter 是使用IDEA自动生成的,仔细观察以上代码,你会发现以下规律:
基本类型自动生成的getter 和setter 方法,名称都是isXXX()和setXXX()形式的。
包装类型自动生成的getter 和setter 方法,名称都是getXXX()和setXXX()形式的。
包装类型自动生成的getter 和setter 方法,名称都是getXXX()和setXXX()形式的。
既然,我们已经达成一致共识使用基本类型boolean 来定义成员变量了,
那么我们再来具体看下Model3 和Model4 中的setter/getter 有何区别。
可以发现,虽然Model3 和Model4 中的成员变量的名称不同,一个是success,另外一个是isSuccess,
但是他们自动生成的getter 和setter 方法名称都是isSuccess和setSuccess。
Java Bean 中关于setter/getter 的规范
关于Java Bean 中的getter/setter 方法的定义其实是有明确的规定的,根据JavaBeans(TM) Specification 规定,
如果是普通的参数propertyName,要以下方式定义其setter/getter:
public <PropertyType> get<PropertyName>();
public void set<PropertyName>(<PropertyType> a);
public void set<PropertyName>(<PropertyType> a);
但是,布尔类型的变量propertyName 则是单独定义的:
public boolean is<PropertyName>();
public void set<PropertyName>(boolean m);
public void set<PropertyName>(boolean m);
通过对照这份JavaBeans 规范发现,在Model4 中,变量名为isSuccess,如果严格按照规范定义的话,他的getter 方法应该叫isIsSuccess。
但是很多IDE 都会默认生成为isSuccess。
那这样做会带来什么问题呢?序列化带来的影响
在一般情况下,其实是没有影响的。
但是有一种特殊情况就会有问题,那就是发生序列化的时候。
关于序列化和反序列化请参考Java 对象的序列化与反序列化。我们这里拿比较常用的JSON 序列化来举例,
看看看常用的fastJson、jackson 和Gson 之间有何区别:
以上代码的Model3 中,只有一个成员变量即isSuccess,
三个方法,分别是IDE 帮我们自动生成的isSuccess 和setSuccess,另外一个是作者自己增加的一个符合getter命名规范的方法。
以上代码输出结果:
Serializable Result With fastjson :{"hollis":"hollischuang","success":true}
Serializable Result With Gson :{"isSuccess":true}
Serializable Result With jackson :{"success":true,"hollis":"hollischuang"}
Serializable Result With Gson :{"isSuccess":true}
Serializable Result With jackson :{"success":true,"hollis":"hollischuang"}
在fastjson 和jackson 的结果中,原来类中的isSuccess 字段被序列化成success,并且其中还包含hollis 值。
而Gson 中只有isSuccess 字段。
由于不同的序列化工具,在进行序列化时使用的策略是不一样的
fastjson 和jackson
在把对象序列化成json 字符串时,是通过反射遍历出该类中的所有getter 方法,得到getHollis 和isSuccess,
然后根据JavaBeans 规则,他会认为这是两个属性hollis 和success 的值。
直接序列化成json:{"hollis":"hollischuang","success":true}。
Gson
但是Gson 并不是这么做的,
他是通过反射遍历该类中的所有属性,并把其值序列化成json:{"isSuccess":true}。
可以看到,由于不同的序列化工具,在进行序列化的时候使用到的策略是不一样的,
所以,对于同一个类的同一个对象的序列化结果可能是不同的。
前面提到的关于对getHollis 的序列化只是为了说明fastjson、jackson 和Gson 之间的序列化策略的不同,
我们暂且把他放到一边,我们把他从Model3 中删除后,重新执行下以上代码,得到结果:
Serializable Result With fastjson :{"success":true}
Serializable Result With Gson :{"isSuccess":true}
Serializable Result With jackson :{"success":true}
Serializable Result With Gson :{"isSuccess":true}
Serializable Result With jackson :{"success":true}
现在,不同的序列化框架得到的json 内容并不相同,
如果对于同一个对象,我使用fastjson 进行序列化,再使用Gson 反序列化会发生什么?
以上代码,输出结果:Model3[isSuccess=false]
这和预期的结果完全相反,原因是因为JSON 框架通过扫描所有的getter 后发现
有一个isSuccess 方法,然后根据JavaBeans 的规范,解析出变量名为success,把
model 对象序列化城字符串后内容为{"success":true}。
有一个isSuccess 方法,然后根据JavaBeans 的规范,解析出变量名为success,把
model 对象序列化城字符串后内容为{"success":true}。
根据{"success":true}这个json 串,Gson 框架在通过解析后,通过反射寻找Model
类中的success 属性,但是Model 类中只有isSuccess 属性,所以,最终反序列化后的
Model 类的对象中,isSuccess 则会使用默认值false。
类中的success 属性,但是Model 类中只有isSuccess 属性,所以,最终反序列化后的
Model 类的对象中,isSuccess 则会使用默认值false。
但是,一旦以上代码发生在生产环境,这绝对是一个致命的问题。
只需要做简单的一件事就可以解决这个问题了
所以,作为开发者,我们应该想办法尽量避免这种问题的发生,
对于POJO 的设计者来说,只需要做简单的一件事就可以解决这个问题了,那就是把isSuccess改为success。
这样,该类里面的成员变量时success,getter 方法是isSuccess,这是完全符合JavaBeans 规范的。
无论哪种序列化框架,执行结果都一样。就从源头避免了这个问题。
引用以下R 大关于Java 开发手册这条规定的评价:
对于这个规定的评价
所以,在定义POJO 中的布尔类型的变量时,不要使用isSuccess 这种形式,而要直接使用success!
Boolean 还是boolean ?
前面介绍完了在success 和isSuccess 之间如何选择,那么排除错误答案后,备选项还剩下:
boolean success
Boolean success
Boolean success
那么,到底应该是用Boolean 还是boolean 来给定一个布尔类型的变量呢?
boolean 是基本数据类型,而Boolean 是包装类型。
在定义一个成员变量时,到底是使用包装类型更好还是使用基本数据类型呢?
来看一段简单的代码:
以上代码输出结果为:default model : Model[success=null, failure=false]
可以看到,当没有设置Model 对象的字段的值时,Boolean 类型的变量会设置默认值为null,而boolean 类型的变量会设置默认值为false。
即对象的默认值是null,boolean 基本数据类型的默认值是false。
在Java 开发手册中,对于POJO 中如何选择变量的类型也有着一些规定:
这里建议我们使用包装类型,原因是什么呢?
举一个扣费的例子
我们做一个扣费系统,扣费时需要从外部的定价系统中,读取一个费率的值,预期该接口的返回值中会包含一个浮点型的费率字段。
当取到这个值得时,就使用公式:金额*费率=费用进行计算,计算结果进行划扣。
如果由于计费系统异常,他可能会返回个默认值
如果这个字段是Double 类型,该默认值为null
如果该字段是double 类型的话,该默认值为0.0
如果扣费系统对于该费率返回值没做特殊处理的话,拿到null 值进行计算会直接报错,阻断程序。
拿到0.0 可能就直接进行计算,得出接口为0 后进行扣费了。这种异常情况就无法被感知。
这种使用包装类型定义变量的方式,通过异常来阻断程序,进而可以被识别到这种线上问题。
如果使用基本数据类型的话,系统可能不会报错,进而认为无异常。
以上,就是建议在POJO 和RPC 的返回值中使用包装类型的原因。
对于布尔类型的变量,我认为可以和其他类型区分开来,作者并不认为使用null 进而导致NPE 是一种最好的实践。
因为布尔类型只有true/false 两种值,完全可以和外部调用方约定好当返回值为false 时的明确语义。
最终达成共识,还是尽量使用包装类型,尽量避免在你的代码中出现不确定的null 值。
总结
在定义一个布尔类型的变量,尤其是一个给外部提供的接口返回值时,要使用success 来命名,
Java 开发手册建议使用封装类来定义POJO 和RPC 返回值中的变量。但是这不意味着
可以随意的使用null,我们还是要尽量避免出现对null 的处理的。
Java 开发手册建议使用封装类来定义POJO 和RPC 返回值中的变量。但是这不意味着
可以随意的使用null,我们还是要尽量避免出现对null 的处理的。
Integer.parseInt(s, radix)实现进制转换解析
自带的Integer.parseInt(s, radix)可以转化十进制为16进制
其中s为输入字符串,radix为进制数
返回值为int型十进制整数。
样例代码
Integer.parseInt(s, radix)
子主题
注意Integer.parseInt 方法的使用
子主题
子主题
常见问题
int和Integer有什么区别?
int默认值是0,Integer默认值是null。
int是基本类型,Integer是引用类型。
Integer范围是-128~127 int的范围是-2^31~(2^31)-1。
包装类型的使用
包装类型为何会提出来?
与基本数据类型的区别?
Java基本语法
基础语法
大小写敏感
Java是对大小写敏感的语言
例如Hello与hello是不同的,这其实就是Java的字符串表示方式
类名
对于所有的类来说,首字母应该大写
例如MyFirstClass
包名
包名应该尽量保证小写
例如my.first.package
方法名
方法名首字母需要小写,后面每个单词字母都需要大写
例如myFirstMethod()
方法的定义
修饰符 返回值类型 方法名(参数列表){
//代码
return 返回值;
}
//代码
return 返回值;
}
修饰符:public static
如果返回值类型为void,则表示没有返回值
参数列表:方法在运算过程中的未知数据,调用者调用方法时传递
return:将方法执行后的结果带给调用者,方法执行到 return ,整体方法运行结束
注意:
在定义一个方法时,要明确返回值和参数列表;
该方法返回值的类型,形参的类型和数量(一个方法只能有一个返回值)
不能在return后面写代码,return代表着方法的结束,后面的代码将永远不会执行
return后面参数的类型要与返回值类型一致
return后面参数的类型要与返回值类型一致
实例
方法的定义实例
形参和实参的区别
形参
在定义函数名和函数体时,使用的参数
目的
用来接收调用该函数时传入的参数
在调用函数时,实参将赋值给形参
实参
可以是常量、变量、表达式、函数等
无论实参是何种类型的量,在函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。
标识符与关键字
标识符
用来标识类名、变量名、方法名、类型名、数组名、文件名的有效字符序列称为标识符。
简单地说,标识符就是一个名字。
关于标识符的语法规则
标识符由字母、下划线、美元符号和数字组成,长度不受限制。
标识符的第一个字符不能是数字字符。
标识符不能是关键字
标识符不能是true、false和null
关键字
关键字就是Java语言中已经被赋予特定意义的一些单词。
不可以把关键字做为标识符来用。
运算符
什么是运算符?
运算符不只Java中有,其他语言也有运算符,
运算符是一些特殊的符号,
主要用于数学函数、一些类 型的赋值语句和逻辑比较方面
Java提供了丰富的运算符
Java的运算符
赋值运算符
赋值运算符使用操作符=来表示
赋值运算符:=
赋值运算符是二目运算符,左面的操作元必须是变量,不能是常量或表达式。
赋值运算符的优先级较低,是14级,结合方向右到左。
赋值表达式的值就是“=”左面变量的值。
把=号右边的值复制给左边
右边的值可以是任何常 数、变量或者表达式
左边的值必须是一个明确的,已经定义的变量。比如int a = 4
复制的不是对象的值,而是对象的引用
对于对象来说,复制的不是对象的值,而是对象的引用
将一个对象复制绐另一个对象,实际上是将一个对象的引用赋值给另一个对象。
注意:不要将赋值运算符“=”与等号逻辑运算符“==”混淆。
算术运算符
算数运算符就和数学中的数值计算差不多
加减运算符: +,-
二目运算符
结合方向是从左到右
操作元是整型或浮点型数据
优先级是4级
乘、除和求余运算符: *,/,%
二目运算符
结合方向是从左到右
操作元是整型或浮点型数据
优先级是3级
图解算数运算符
图解算数运算符
算术表达式
用算术符号和括号连接起来的符合java语法规则的式子
优先级问题
当一个表达式中存在多个操作符时,操作符的优先级顺序就 决定了计算顺序
最简单的规则就是先乘除后加减,()的优先级最高
没必要记住所有的优先级顺 序,不确定的直接用0就可以了
算术混合运算的精度
精度从“低”到“高”排列顺序
byte short char int long float double
Java在计算算术表达式的值时,使用下列计算精度规则
1.如果表达式中有双精度浮点数(double型数据),则按双精度进行运算。
2.如果表达式中最高精度是单精度浮点数(float型数据),则按单精度进行运算。
3.如果表达式中最高精度是long型整数,则按long精度进行运算。
4.如果表达式中最高精度低于int型整数,则按int精度进行运算。
自增、自减运算符
举例
自增、自减运算符:++,--
单目运算符
可以放在操作元之前,也可以放在操作元之后
操作元必须是一个整型或浮点型变量。
作用是使变量的值增1或减1
++x(--x)表示在使用x之前,先使x的值增(减)1
x++(x--)表示在使用x之后,使x的值增(减)1
比较运算符/关系运算符
用于程序中的变量之间,变量和自变量之间以及其他类型的信息之间的比较。
二目运算符,用来比较两个值的关系。
关系/比较运算符的运算结果是boolean型
当运算符对应的关系成立时,运算结果是true,否则是false。
共有6个,通常作为判断的依据用于条件语句中。
图解比较运算符
图解比较运算符
=与==的区别,错题集
if(x=y)是将y赋值给x,但是数据类型是int类型的,编译不能通过,
如果把代码改为这样:
boolean x = false;
boolean y = ture;
if(x=y){...}这样就就不会报错了,编译正常通过。
1、Java中,赋值是有返回值的 ,赋什么值,就返回什么值。比如这题,x=y,返回y的值,所以括号里的值是1。
2、Java跟C的区别,C中赋值后会与0进行比较,如果大于0,就认为是true;而Java不会与0比较,而是直接把赋值后的结果放入括号。
子主题
子主题
由于java和C语言的不同处理机制导致的:
C语言中
当if语句中的条件为赋值语句时,实际上是将赋值后的结果与0进行比较【左值】
if(1) 由于1>0 所以认为是true
当if语句中的条件为赋值语句时,实际上是将赋值后的结果与0进行比较【左值】
if(1) 由于1>0 所以认为是true
java语言中
虽然也用了左值,但是不再与0比较,而是直接将0放入if()中
但是int类型,不能转换为boolean,所以会报错:“ Type mismatch: cannot convert from int to boolean ”
虽然也用了左值,但是不再与0比较,而是直接将0放入if()中
但是int类型,不能转换为boolean,所以会报错:“ Type mismatch: cannot convert from int to boolean ”
if()语句括号中为比较表达式,返回值要么是true,要么是false,
if(x=y)是将y赋值给x,但是数据类型是int类型的,编译不能通过,
如果把代码改为这样:
boolean x = false;
boolean y = ture;
if(x=y){...}这样就就不会报错了,编译正常通过。
1、Java中,赋值是有返回值的 ,赋什么值,就返回什么值。比如这题,x=y,返回y的值,所以括号里的值是1。
2、Java跟C的区别,C中赋值后会与0进行比较,如果大于0,就认为是true;而Java不会与0比较,而是直接把赋值后的结果放入括号。
逻辑运算符
主要有三种
与、或、非
&&,||,!
其中&&、||为二目运算符,实现逻辑与、逻辑或;
!为单目运算符,实现逻辑非
操作元必须是boolean型数据
可以用来连接关系表达式
逻辑运算符
下面是逻辑运算符对应的true/false符号表
逻辑运算符对应的true/false符号表
逻辑运算符对应的true/false符号表
讲讲&和&&的区别
&是逻辑与,&&是短路与。
短路与只要与左边的条件为false就能判断整个条件为false,而逻辑与两边都需要判断。
按位运算符/位运算符
对两个整型数据实施位运算,即对两个整型数据对应的位进行运算得到一个新的整型数据。
按位运算符用来操作整数基本类型中的每个比特位,也就是二进制位。
按位操作符会对两个参数中对 应的位执行布尔代数运算,并最终生成一个结果。
图解
图解
如果比较的双方是数字,那么进行比较就会变成按位运算符
按位与
按位进行与运算(AND)
两个操作数中位都为1,结果才为1,否则结果为0。
需要首先把比 较双方转换成二进制再按每个位进行比较
“按位与”运算符“&”是双目运算符
按位或
按位进行或运算(OR)
两个位只要有一个为1,那么结果就是1,否则就为0。
“按位或”运算符:“|”是二目运算符
按位非
按位进行异或运算(XOR)
如果位为0,结果是1,如果位为1,结果是0。
“按位非”运算符:“~”是单目运算符。
按位异或
按位进行取反运算(NOT)
两个操作数的位中,相同则结果为0,不同则结果为1。
“按位异或”运算符:“^”是二目运算符。
移位运算符
移位运算符用来将操作数向某个方向(向左或者右)移动指定的二进制位数。
图解
图解
三元运算符
三元运算符是类似if...else...这种的操作符
语法为:条件表达式?表达式1:表达式2。
问号 前面的位置是判断的条件,判断结果为布尔型,为true时调用表达式1,为false时调用表达式2。
instanceof 运算符
instanceof 运算符是二目运算符
instanceof是双目运算符
类似于==,>,< 等操作符。
instanceof 是Java 的一个二元操作符,
instanceof运算符是Java独有的运算符号。
instanceof 是Java 的保留关键字
它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean 的数据类型。
其左面的操作元是一个对象,右面的操作元是一个类,
当左面的对象是右面的类或子类创建的对象时,该运算符运算的结果是true ,否则是false。
当左面的操作元是右面的类或子类所创建的对象时,instanceof运算的结果是true,否则是false。
以下实例创建displayObjectClass() 方法来演示Java instanceof 关键字用法
演示Java instanceof 关键字用法
运算符的优先级
运算符的优先级,决定了表达式中运算执行的先后顺序
在编写程序时,尽量使用括号()运算符号,来实现想要的运算次序,以免产生难以阅读或含糊不清的计算顺序
运算符的结合性,决定了并列的相同级别运算符的先后顺序
运算符的优先级和结合性
运算符的优先级和结合性
表达式
Java的表达式就是用运算符连接起来的符合Java规则的式子。
与Null有关的事
说明
对于Java程序员来说,空指针一直是恼人的问题,在开发中经常会受到NullPointerException的 蹂躏和壁咚。
Java的发明者也承认这是一个巨大的设计错误。
那么关于null,你应该知道下面这几件事情来有效的了解null,从而避免很多由null引起的错误。
特性
大小写敏感
首先,null是Java中的关键字,像是public、static、final。
它是大小写敏感的,不能将null写成Null或NULL,编辑器将不能识别它们然后报错。
这个问题已经几乎不会出现,因为eclipse和Idea编译器已经绐出了编译器提示,所以不用考虑这个问题。
null是任何引用类型的初始值
null是所有引用类型的默认值,Java中的任何引用变量都将null作为默认值
所有Object类下的引用类型默认值都是null。
这对所有的引用变量都适用。
就像是基本类型的默认值一样,例如int 的默认值是0, boolean的默认值是false。
null只是一种特殊的值
null既不是对象也不是一种类型
它仅是一种特殊的值
可将它赋予任何类型,可将null转换 为任何类型
在编译期和运行期内,将null转换成任何的引用类型都是可行的,并且不会抛出空指针异常。
举例
null只能赋值给引用变量,不能赋值给基本类型变量。
持有null的包装类在进行自动拆箱时,不能完成转换,会抛出空指针异常,并且null也不能和基本数据类型进行对比
举例
使用了带有null值的引用类型变量,instanceof操作会返回false
说明
这是instanceof操作符一个很重要的特性,使得对类型强制转换检查很有用
静态变量为null调用静态方法不会抛出NullPointerException。因为静态方法使用了静态绑定。
关于null的几种处理方式
使用Null-Safe方法
使用null-safe安全的方法
java类库中有很多工具类都提供了静态方法,例如基本数据类型的 包装类。
尽量使用对象的静态方法
例如
number没有赋值,所以默认为null
使用String.value(number)静态方法,没有抛出空指针异常,
但是使用toString()却抛出了空指针异常。
null判断
可以使用==或者!=操作来比较null值,但是不能使用其他算法或者逻辑操作,例如小于或者 大于。
跟SQL不一样,在Java中null == null将返回true,
如下所示:
如下所示:null判断
图解关于null的几种处理方式
图解关于null的几种处理方式
Java中的六类语句概述
1.方法调用语句
如:System.out.println(" Hello");
2.表达式语句
表示式尾加上分号。
比如赋值语句:x=23;
3.复合语句
可以用{ }把一些语句括起来构成复合语句
{
z=123+x;
System.out.println("How are you");
}
z=123+x;
System.out.println("How are you");
}
4.空语句
一个分号也是一条语句,称做空语句。
5.控制语句
Java执行控制流程
Java中的控制流程其实和C —样,
在Java中,流程控制会涉及多种
控制语句分为
条件语句/条件分支语句
可根据不同的条件执行不同的语句
条件分支语句按着语法格式可细分为三种形式
if条件语句
if条件语句
if语句可以单独判断表达式的结果,
表示表达的执行结果
if语句是单条件分支语句,即根据一个条件来控制程序执行的流程。
if 语句的语法格式
if(表达式){
若干语句
}
若干语句
}
if 语句的语法格式
if...else条件语句
if语句还可以与else连用,
通常表现为如果满足某种条件,就进行某种处理,否则就进行另一种处理。
if-else 语句是双条件分支语句,即根据一个条件来控制程序执行的流程。
if后的0内的表达式必须是boolean型的。
如果为true,则执行if后的复合语句;
如果为false,则执 行else后的复合语句。
if-else 语句的语法格式:
if(表达式) {
若干语句
} else {
若干语句
}
若干语句
} else {
若干语句
}
有语法错误的if-else语句 :×
if(x>0)
y=10;
z=20;
else
y=-100;
if(x>0)
y=10;
z=20;
else
y=-100;
正确的写法是:√
if(x>0){
y=10;
z=20;
}
else
y=100;
if(x>0){
y=10;
z=20;
}
else
y=100;
图解if...else条件语句
if...else条件语句
if...else if多分支语句
上面中的if...else是单分支和两个分支的判断
如果有多个判断条件,就需要使用if...else if
多条件分支语句,即根据多个条件来控制程序执行的流程。
if...else if多分支语句的语法格式:
if(表达式) {
若干语句
} else if(表达式) {
若干语句
}
… …
else {
若干语句
}
若干语句
} else if(表达式) {
若干语句
}
… …
else {
若干语句
}
图解if...else if多分支语句
图解if...else if多分支语句
开关语句
在条件判断语句(if语句)过多时,可以使用开关语句来编写。
switch多分支语句
一种比if...else if语句更优雅的方式是使用switch多分支语句
switch开关语句
switch 语句是单条件多分支的开关语句,:
开关语句的基本结构是:它的一般格式定义如下
switch(整数){
case 整数值 1: 语句; break;
case 整数值 2: 语句; break;
case 整数值 3: 语句; break;
……………………..
default: 语句;
}
case 整数值 1: 语句; break;
case 整数值 2: 语句; break;
case 整数值 3: 语句; break;
……………………..
default: 语句;
}
switch(表达式) {
case 常量值1:
若干个语句
break;
case 常量值2:
若干个语句
break;
...
case 常量值n:
若干个语句
break;
default:
若干语句
}
case 常量值1:
若干个语句
break;
case 常量值2:
若干个语句
break;
...
case 常量值n:
若干个语句
break;
default:
若干语句
}
(其中break语句是可选的)
循环语句
在满足一定的条件下,反复执行某一表达式的操作,直到满足循环语句的要求。
包括
while循环语句
利用一个条件来控制是否要继续反复执行这个语句。
while循环语句的格式
while循环语句的格式
当(布尔值)为true候,执行下面的表达式,
布尔值为false时,结束循环,
布尔 值其实也是一个表达式
布尔 值其实也是一个表达式
while语句的语法格式
while (表达式) {
若干语句
}
若干语句
}
while语句的执行规则
(1)计算表达式的值,如果该值是true时,就进行(2),否则执行(3)。
(2)执行循环体,再进行(1)。
(3)结束while语句的执行。
(2)执行循环体,再进行(1)。
(3)结束while语句的执行。
图解while循环语句
图解while循环语句
do...while 循环
while与do...while循环的唯一区别是do...while语句至少执行一次,即使第一次的表达式为false。
在while循环中,如果第一次条件为false,那么其中的语句根本不会执行。
在实际应用中,while要比 do...while应用的更广。
一般形式如下
do...while 循环
do-while语句的语法格式
do {
若干语句
} while(表达式);
若干语句
} while(表达式);
do- while语句的执行规则
(1)执行循环体,再进行(2)。
(2)计算表达式的值,如果该值是true时,就进行(1),否则执行(3)。
(3)结束while语句的执行。
图解do...while 循环
图解do...while 循环
for循环语句
for循环是我们经常使用的循环方式,这种形式会在第一次迭代前进行初始化。
它的形式如下,for语句的语法格式
for循环语句
for (表达式1; 表达式2; 表达式3) {
若干语句
}
若干语句
}
每次迭代前会测试布尔表达式。如果获得的结果是false,就会执行for语句后面的代码;每次循环结 束,会按照步进的值执行下一次循环。
逗号操作符
这里不可忽略的一个就是逗号操作符
Java里唯一用到逗号操作符的就是for循环控制语句
在表达式 的初始化部分,可以使用一系列的逗号分隔的语句
通过逗号操作符,可以在for语句内定义多个变 量,但它们必须具有相同的类型
举例
举例
for语句的执行规则
(1)计算“表达式1”,完成必要的初始化工作
(2)判断“表达式2”的值,若“表达式2”的值为true,则进行(3),否则进行(4)
(3)执行循环体,然后计算“表达式3”,以便改变循环条件,进行(2)
(4)结束for语句的执行
for循环语句
for-each 语句
在Java JDK 1.5中还引入了一种更加简洁的、方便对数组和集合进行遍历的方法,即for-each语 句
举例
for-each 语句
跳转语句
Java语言中的三种跳转语句
break、continue和return
用关键break或continue加上分号构成的语句
在循环体中可以使用break语句和continue语句
包括
break语句
在switch中已经见到了,它是用于终止循环的操作
如果在某次循环中执行了break语句,那么整个循环语句就结束。
实际上break语句在for、while、 do while循环语句中,用于强行退出当前循环
break语句
continue 语句
continue也可以放在循环语句中
它与break语句具有相反的效果
如果在某次循环中执行了continue语句,那么本次循环就结束,即不再执行本次循环中循环体中continue语句后面的语句,而转入进行下一次循环。
它的作用是用于执行下一次循环, 而不是退出当前循环
continue 语句的使用
continue 语句
continue 语句的使用
continue 语句
return语句
return语句可以从一个方法返回,并把控制权交绐调用它的语句。
return语句
6.package语句和 import语句
和类、对象有关
import 语句
一个类可能需要另一个类声明的对象作为自己的成员或方法中的局部变量,如果这两个类在同一个包中,当然没有问题。
如果一个类想要使用的那个类和它不在一个包中,要使用import语句完成使命。
引入类库中的类
如果用户需要类库中的类就必须使用import语句
如: import java.util.Date;
引入自定义包中的类
用户程序可以使用tom.jiafei包中的类
如:import tom.jiafei.*;
使用非类库中有包名的类,也要使用import语句。
使用无包名的类
在源文件中一直没有使用包语句,因此各个源文件得到的类都没有包名。
如果一个源文件中的类想使用无名包中的类,只要将这个无包名的类的字节码和当前类保存在同一目录中即可。
避免类名混淆
区分无包名和有包名的类
如果一个源文件使用了一个无名包中的A类,同时又用import语句引入了某个有包名的同名的类,如tom.jiafei中的A类,就可能引起类名的混淆。
区分有包名的类
如果一个源文件引入了两个包中同名的类,那么在使用该类时,不允许省略包名 。
package语句
包是Java语言中有效地管理类的一个机制
包名的目的
有效的区分名字相同的类
不同Java源文件中两个类名字相同时,它们可以通过隶属不同的包来相互区分。
包语句
通过关键字package声明包语句。
package语句作为Java源文件的第一条语句,为该源文件中声明的类指定包名。
package语句的一般格式为
package 包名;
例如
package sunrise;
package sun.com.cn;
有包名的类的存储目录
如果一个类有包名,那么就不能在任意位置存放它,否则虚拟机将无法加载这样的类。
程序如果使用了包语句
例如:package tom.jiafei;
那么存储文件的目录结构中必须包含有如下的结构
…\tom\jiafei
如c:\1000\tom\jiafei
并且要将源文件编译得到的类的字节码文件保存在目录
c:\1000\tom\jiafei中
(源文件可以任意存放)
运行有包名的主类
如果主类的包名是
tom.jiafei
那么主类的字节码一定存放在该目录中
…\tom\jiefei
运行时必须到{ }的上一层(即tom的父目录)目录中去运行主类。
tom\jiefei
假设tom\jiefei的上一层目录是1000,那么,必须如下格式来运行: C:\1000\java tom.jiafei.主类名
注:主类名是:“包名.主类名”
Java中的各种变量
实例变量
是什么?
又被称为Instance variables
在声明成员变量时,用关键字static给予修饰的称作类变量,否则称作实例变量
class Dog {
float x; //实例变量
static int y; //类变量
}
float x; //实例变量
static int y; //类变量
}
如何识别实例变量?如何知道一个变量它是实例变量呢?实例变量的定义规则
实例变量可以使用四种访问修饰符修饰
public
protected
default
private
实例变量可以使用以下关键字进行修饰
transient
final
实例变量不可以使用以下关键字进行修饰
abstract
synchronized
strict fp
native
static
实例变量带有默认值,实例变量不用初始化就能使用
实例变量不用强制初始化,它有自己的默认值。
常用实例变量的初始值
在任何方法、构造方法、块之外的变量都是实例变量
实例变量的只能在类中声明,但是在方法、构造函数或任何块之外。
当在为堆中对象分配空间时,将为每个实例变量分配一块区域。
实例变量只能通过创建对象来使用
当使用new关键字进行创建对象时, 实例变量同时也被创建
当垃圾回收器回收对象时,实例变量也会被销毁
每个对象都有自己的一个实例变量的副本
因此在一个对象中修改变量,不会对其他对象中的实例变量造成影响
实例变量不会在实例之间共享,每一个对象的实例都有自己的一个实例变量
实例变量只能通过创建对象引用来使用。
实例变量都是基于特定实例的
实例变量和类变量的区别
不同对象的实例变量互不相同
所有对象共享类变量
通过类名直接访问类变量
实例变量例子
实例变量
实例变量的调用例子
实例变量的调用
全局变量(Java中不存在)
是什么?
又被称为Global variables
其他语言(C、C++)才存在全局变量
如果你有其他语言的编程经验, 比如C、C++的话
创建全局变量例子
你会接触到全局变量这个概念
C、C++中创建全局变量例子
C、C++中创建全局变量例子
在Java中, 是不存在全局变量的,Why?
因为Java是一门面向对象的编程语言, 所有的内容都是属于类的一部分。
Java这么做的原因是:为了防止数据和类成员被其他程序的其他部分有意或者无意的修改。
在Java中, 使用静态变量来起到全局访问的目的
静态变量
是什么?
又被称为Static variables
静态变量的定义比较简单
静态变量是属于该类的变量
静态变量的特点
静态变量只能使用static关键字进行修饰
它不能在方法中进行声明, 不论是静态方法还是非静态方法。
它是由static关键字来修饰的
static修饰的变量属于静态变量
它只能定义在类的内部、方法的外部。
图解
静态变量只能使用static关键字进行修饰
静态变量会在程序运行前进行初始化,并且只初始化一次。
静态变量会有一个初始化顺序
静态变量的所有实例共享同一个副本
静态变量只有一个
它不会随着对象实例的创建而进行副本拷贝
静态变量可以通过类名.变量名进行访问,并且不需要创建任何对象就能访问
举例
静态变量可以通过类名.变量名进行访问
可以在非静态方法中使用静态变量
类变量(Java中类变量就是静态变量)
是什么?
又被称为Class variables
类变量,也称为static变量,静态变量
在Java中, 类变量就是静态变量, 它们都用static关键字进行修饰
如果你再听到说静态变量时,它也就是类变量。
类变量存放在JVM的方法区
常量(静态变量+final关键字)
类变量(静态属性/静态变量), 可以添加关键字final来表示常量。
局部变量
局部变量是什么?
又称为Local variables
指的是在方法中、构造器中或者块代码中定义的变量。
不管上面的一些变量概念如何变换、局部变量都站如松,坐如钟,行如风,卧如弓,从容应对各种不同文章的比较。
真是一个省事的变量。
在方法体中声明的变量和方法的参数
方法的基本组成
方法声明
方法名称
方法的参数
方法的返回类型,返回值
方法体
局部变量的有效范围
生命周期随方法、构造器、代码块的执行完毕而销毁。
局部变量只在声明它的方法内有效
局部变量的有效性与其声明的位置有关。
方法的参数在整个方法内有效
方法内的局部变量从声明它的位置之后开始有效
如果局部变量的声明是在一个复合语句中,那么该局部变量的有效范围是该复合语。
局部变量存放在JVM的栈内存中
特点
局部变量定义在方法、构造器或者代码块中
局部变量的生命周期随方法、构造器、代码块的执行完毕而销毁
不能使用访问修饰符
例如
局部变量不能使用访问修饰符
局部变量仅在方法的声明、构造函数或者块内可见
局部变量只能在调用这些方法、构造函数或者块的内部使用
局部变量没有默认值
局部变量应该在第一次使用或者声明时,就应该初始化完成
例如
局部变量应该在第一次使用或者声明时,就应该初始化完成
成员变量(统称)
(StackFlow的网友回复)实例变量和类变量都称为成员变量
变量声明部分所声明的变量,被称做域变量或成员变量
成员变量存放在JVM的堆内存
类的实现包括两部分
类声明
例如:
class Vehicle {
……
}
class Vehicle {
……
}
“class Vehicle”称作类声明;
“Vehicle”是类名。
class 类名
类体
类声明之后的一对大括号“{”,“}”以及它们之间的内容称作类体,大括号之间的内容称作类体的内容。
类体的内容由两部分构成
变量的声明
刻画属性
变量声明部分所声明的变量,被称做域变量或成员变量
方法的定义
刻画功能。
JDK官网手册中关于Variables变量的定义
在Java中, 只有三中类型的变量
定义在类中的成员变量
属性
定义在方法(包含构造方法)或者块代码中的变量
局部变量
定义在方法定义中的变量
参数
在Java中, 只有下面几种类型的变量
实例变量(非静态属性)
非静态属性也就被称为实例变量,因为它们的值是相对于每个实例来说的。
换句话说,对于每个对象来讲,实例变量的值都是唯一的;
定义在构造方法、代码块、方法外的变量被称为实例变量
实例变量的副本数量和实例的数量一样。
类变量(静态属性)
类变量就是使用static修饰符声明的字段
这就会告诉编译器:无论该类被实例化了多少次, 该变量只存在一个副本。
另外, 可以添加关键字final来表示常量。
局部变量
没有特殊的关键字将制定的变量声明为局部变量、确定其声明的完全取决于声明变量的位置。
定义在方法、构造方法、代码块内的变量被称为局部变量;
参数
定义在方法参数中的变量被称为参数
平常用到最多的方法是什么方法?当然是main方法啊,
main方法是怎么定义的?
public static void main(String args)
中的args是不是就是Strng的数组的变量, 也称其为参数,
参数也没有关键字进行声明,标识其为参数也只是取决于其声明位置。
成员变量的类型
可以是Java中的任何一种数据类型
基本类型
引用类型:数组、对象和接口
成员变量的有效范围
成员变量在整个类内都有效
其有效性与它在类体中书写的先后位置无关。
成员变量和方法作用域
对于成员变量和方法的作用域,public,protected,private 以及不写之间的区别:
public : 表明该成员变量或者方法是对所有类或者对象都是可见的,所有类或者对象都可以直接访问。
private : 表明该成员变量或者方法是私有的,只有当前类对其具有访问权限,除此之外其他类或者对象都没有访问权限.子类也没有访问权限。
protected : 表明成员变量或者方法对类自身,与同在一个包中的其他类可见,其他包下的类不可访问,除非是他的子类。
default : 表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类。
成员变量的隐藏
对于子类可以从父类继承的成员变量,
只要子类中声明的成员变量和父类中的成员变量同名时,子类就隐藏了继承的成员变量,
子类自己声明定义的方法操作与父类同名的成员变量是指子类重新声明定义的这个成员变量。
区分成员变量和局部变量
如果局部变量的名字与成员变量的名字相同,则成员变量被隐藏,即这个成员变量在这个方法内暂时失效
如果想在该方法中使用被隐藏的成员变量,必须使用关键字this
注意事项
对成员变量的操作只能放在方法中,
方法可以对成员变量和该方法体中声明的局部变量进行操作。
在声明成员变量时,可以同时赋予初值,但是不可以在类体中有单独的赋值语句
不可以有变量的声明和方法的定义以外的其它语句
Java中到底有哪些变量
用static来定义变量
只能是类变量、或者说静态变量
而且其定义位置只能在类中, 方法或代码块外,变量的副本只有一个。
不用static来声明变量,则有三种变量的叫法
定义在构造方法、代码块、方法外的变量被称为实例变量,实例变量的副本数量和实例的数量一样。
定义在方法、构造方法、代码块内的变量被称为局部变量;
定义在方法参数中的变量被称为参数。
图解Java中到底有哪些变量
图解Java中到底有哪些变量
图解Java中到底有哪些变量
上面定义的三个变量中
变量a 就是类变量
变量b 就是成员变量
而变量c 和d 是局部变量
变量的编程风格
(1)一行只声明一个变量。
(2)变量的名字符合标识符规定。
(3)变量名字见名知意,避免容易混淆的变量名字。
if-else代码优化的N种方案
如何优化if else ?
代码中如果if-else比较多,阅读起来比较困难,维护起来也比较困难,很容易出bug
优化方案一:提前return,去除不必要的else
如果if-else代码块包含return语句,可以考虑通过提前return,把多余else干掉,使代码更加优雅。
卫语句
卫语句(guard clauses)是一种改善嵌套代码的优化代码。
将经过多级嵌套的代码使用卫语句优化之后,代码嵌套层数可以降低,因此改使用卫语句能降低代码的复杂程度。
卫语句是通过对原条件进行逻辑分析,将某些要害(guard)条件优先作判断,从而简化程序的流程走向,因此称为卫语句。
卫语句往往用于对 if 条件嵌套代码的优化。
卫语句的概念在拥有 if 语句的算法语言中更常见,如 Java、C++、C、Python、JavaScript 等。
比如
优化前
if(condition){
//doSomething
}else{
return ;
}
//doSomething
}else{
return ;
}
优化后
if(!condition){
return ;
}
//doSomething
return ;
}
//doSomething
比如
下面的 Java 代码的最大嵌套层数是 3(方法本身也算一层)
子主题
当使用了卫语句进行重构之后,最大嵌套层数变成了 2
子主题
比如
子主题
优化方案二:使用条件三目运算符
使用条件三目运算符可以简化某些if-else,使代码更加简洁,更具有可读性。
优化前
int price ;
if(condition){
price = 80;
}else{
price = 100;
}
if(condition){
price = 80;
}else{
price = 100;
}
优化后
int price = condition?80:100;
优化方案三:使用枚举
在某些时候,使用枚举也可以优化if-else逻辑分支,按个人理解,它也可以看做一种表驱动方法。
优化前
String OrderStatusDes;
if(orderStatus==0){
OrderStatusDes ="订单未支付";
}else if(OrderStatus==1){
OrderStatusDes ="订单已支付";
}else if(OrderStatus==2){
OrderStatusDes ="已发货";
}
...
if(orderStatus==0){
OrderStatusDes ="订单未支付";
}else if(OrderStatus==1){
OrderStatusDes ="订单已支付";
}else if(OrderStatus==2){
OrderStatusDes ="已发货";
}
...
优化后
先定义一个枚举
public enum OrderStatusEnum {
UN_PAID(0,"订单未支付"),PAIDED(1,"订单已支付"),SENDED(2,"已发货"),;
private int index;
private String desc;
public int getIndex() {
return index;
}
public String getDesc() {
return desc;
}
OrderStatusEnum(int index, String desc){
this.index = index;
this.desc =desc;
}
OrderStatusEnum of(int orderStatus) {
for (OrderStatusEnum temp : OrderStatusEnum.values()) {
if (temp.getIndex() == orderStatus) {
return temp;
}
}
return null;
}
}
UN_PAID(0,"订单未支付"),PAIDED(1,"订单已支付"),SENDED(2,"已发货"),;
private int index;
private String desc;
public int getIndex() {
return index;
}
public String getDesc() {
return desc;
}
OrderStatusEnum(int index, String desc){
this.index = index;
this.desc =desc;
}
OrderStatusEnum of(int orderStatus) {
for (OrderStatusEnum temp : OrderStatusEnum.values()) {
if (temp.getIndex() == orderStatus) {
return temp;
}
}
return null;
}
}
有了枚举之后,以上if-else逻辑分支,可以优化为一行代码
String OrderStatusDes = OrderStatusEnum.0f(orderStatus).getDesc();
优化方案四:合并条件表达式
如果有一系列条件返回一样的结果,可以将它们合并为一个条件表达式,让逻辑更加清晰。
优化前
double getVipDiscount() {
if(age<18){
return 0.8;
}
if("深圳".equals(city)){
return 0.8;
}
if(isStudent){
return 0.8;
}
//do somethig
}
if(age<18){
return 0.8;
}
if("深圳".equals(city)){
return 0.8;
}
if(isStudent){
return 0.8;
}
//do somethig
}
优化后
double getVipDiscount(){
if(age<18|| "深圳".equals(city)||isStudent){
return 0.8;
}
//doSomthing
}
if(age<18|| "深圳".equals(city)||isStudent){
return 0.8;
}
//doSomthing
}
优化方案五:使用 Optional
有时候if-else比较多,是因为非空判断导致的,这时候你可以使用java8的Optional进行优化。
优化前
String str = "jay@huaxiao";
if (str != null) {
System.out.println(str);
} else {
System.out.println("Null");
}
if (str != null) {
System.out.println(str);
} else {
System.out.println("Null");
}
优化后
Optional<String> strOptional = Optional.of("jay@huaxiao");
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));
优化方案六:表驱动法
表驱动法,又称之为表驱动、表驱动方法。
表驱动方法是一种使你可以在表中查找信息,而不必用很多的逻辑语句(if或Case)来把它们找出来的方法。
以下的demo,把map抽象成表,在map中查找信息,而省去不必要的逻辑语句。
优化前
if (param.equals(value1)) {
doAction1(someParams);
} else if (param.equals(value2)) {
doAction2(someParams);
} else if (param.equals(value3)) {
doAction3(someParams);
}
// ...
doAction1(someParams);
} else if (param.equals(value2)) {
doAction2(someParams);
} else if (param.equals(value3)) {
doAction3(someParams);
}
// ...
优化后
Map<?, Function<?> action> actionMappings = new HashMap<>(); // 这里泛型 ? 是为方便演示,实际可替换为你需要的类型
// 初始化
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
// 省略多余逻辑语句
actionMappings.get(param).apply(someParams);
// 初始化
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
// 省略多余逻辑语句
actionMappings.get(param).apply(someParams);
优化方案七:优化逻辑结构,让正常流程走主干
优化前
public double getAdjustedCapital(){
if(_capital <= 0.0 ){
return 0.0;
}
if(_intRate > 0 && _duration >0){
return (_income / _duration) *ADJ_FACTOR;
}
return 0.0;
}
if(_capital <= 0.0 ){
return 0.0;
}
if(_intRate > 0 && _duration >0){
return (_income / _duration) *ADJ_FACTOR;
}
return 0.0;
}
优化后
public double getAdjustedCapital(){
if(_capital <= 0.0 ){
return 0.0;
}
if(_intRate <= 0 || _duration <= 0){
return 0.0;
}
return (_income / _duration) *ADJ_FACTOR;
}
if(_capital <= 0.0 ){
return 0.0;
}
if(_intRate <= 0 || _duration <= 0){
return 0.0;
}
return (_income / _duration) *ADJ_FACTOR;
}
将条件反转使异常情况先退出,让正常流程维持在主干流程,可以让代码结构更加清晰。
优化方案八:策略模式+工厂方法消除if else
假设需求为,根据不同勋章类型,处理相对应的勋章服务,
优化前有以下代码
String medalType = "guest";
if ("guest".equals(medalType)) {
System.out.println("嘉宾勋章");
} else if ("vip".equals(medalType)) {
System.out.println("会员勋章");
} else if ("guard".equals(medalType)) {
System.out.println("展示守护勋章");
}
...
if ("guest".equals(medalType)) {
System.out.println("嘉宾勋章");
} else if ("vip".equals(medalType)) {
System.out.println("会员勋章");
} else if ("guard".equals(medalType)) {
System.out.println("展示守护勋章");
}
...
优化后的代码
首先,我们把每个条件逻辑代码块,抽象成一个公共的接口,可以得出以下代码:
//勋章接口
public interface IMedalService {
void showMedal();
}
public interface IMedalService {
void showMedal();
}
我们根据每个逻辑条件,定义相对应的策略实现类,可得以下代码:
//守护勋章策略实现类
public class GuardMedalServiceImpl implements IMedalService {
@Override
public void showMedal() {
System.out.println("展示守护勋章");
}
}
//嘉宾勋章策略实现类
public class GuestMedalServiceImpl implements IMedalService {
@Override
public void showMedal() {
System.out.println("嘉宾勋章");
}
}
//VIP勋章策略实现类
public class VipMedalServiceImpl implements IMedalService {
@Override
public void showMedal() {
System.out.println("会员勋章");
}
}
public class GuardMedalServiceImpl implements IMedalService {
@Override
public void showMedal() {
System.out.println("展示守护勋章");
}
}
//嘉宾勋章策略实现类
public class GuestMedalServiceImpl implements IMedalService {
@Override
public void showMedal() {
System.out.println("嘉宾勋章");
}
}
//VIP勋章策略实现类
public class VipMedalServiceImpl implements IMedalService {
@Override
public void showMedal() {
System.out.println("会员勋章");
}
}
接下来,我们再定义策略工厂类,用来管理这些勋章实现策略类,如下:
//勋章服务工产类
public class MedalServicesFactory {
private static final Map<String, IMedalService> map = new HashMap<>();
static {
map.put("guard", new GuardMedalServiceImpl());
map.put("vip", new VipMedalServiceImpl());
map.put("guest", new GuestMedalServiceImpl());
}
public static IMedalService getMedalService(String medalType) {
return map.get(medalType);
}
}
public class MedalServicesFactory {
private static final Map<String, IMedalService> map = new HashMap<>();
static {
map.put("guard", new GuardMedalServiceImpl());
map.put("vip", new VipMedalServiceImpl());
map.put("guest", new GuestMedalServiceImpl());
}
public static IMedalService getMedalService(String medalType) {
return map.get(medalType);
}
}
使用了策略+工厂模式之后,代码变得简洁多了,如下:
public class Test {
public static void main(String[] args) {
String medalType = "guest";
IMedalService medalService = MedalServicesFactory.getMedalService(medalType);
medalService.showMedal();
}
}
public static void main(String[] args) {
String medalType = "guest";
IMedalService medalService = MedalServicesFactory.getMedalService(medalType);
medalService.showMedal();
}
}
过多条件语句的替换方案-策略模式
子主题
优化方案九:策略模式 + 反射解决方法
业务场景
外包企业的审批人需要审批打卡的场景;
审批人分为多种不同的级别,多种级别中具有方式相同但是内容不同的操作:审批。
审批人分为多种不同的级别,多种级别中具有方式相同但是内容不同的操作:审批。
原来场景
有前端传来审批人参数,使用if-else 来判断该审批人的级别属于哪一个级别,执行相应的审批方法。
每一个审批方法写在了业务类底,命名采用1级审批,2级审批---等等的命名方式来命名。
问题
if - else 逻辑复杂,不易阅读,函数中审批能抽象的地方未抽象,
test测试非常麻烦,新增审批人员需要对代码进行修改,违背开闭原则。
解决过程
抽象审批中的原子操作的代码,比如查询对应级别所审批的同学名单。代码变得好看一些,但是任然未解决,if - else 和 多个 审批方法的实现。
尝试策略模式
使用策略模式将每个级别的用户抽象起来
子主题
Main
package strategy;
public class Main {
public static void main(String[] args) {
if (LevelEnum.LEVEL1.equals(1)) {
SupervisorControl supervisorControl = new SupervisorControl(new Supervisor1());
supervisorControl.execute(1);
}
if (LevelEnum.LEVEL2.equals(2)) {
SupervisorControl supervisorControl = new SupervisorControl(new Supervisor2());
supervisorControl.execute(2);
}
if (LevelEnum.LEVEL3.equals(3)) {
SupervisorControl supervisorControl = new SupervisorControl(new Supervisor3());
supervisorControl.execute(3);
}
}
}
public class Main {
public static void main(String[] args) {
if (LevelEnum.LEVEL1.equals(1)) {
SupervisorControl supervisorControl = new SupervisorControl(new Supervisor1());
supervisorControl.execute(1);
}
if (LevelEnum.LEVEL2.equals(2)) {
SupervisorControl supervisorControl = new SupervisorControl(new Supervisor2());
supervisorControl.execute(2);
}
if (LevelEnum.LEVEL3.equals(3)) {
SupervisorControl supervisorControl = new SupervisorControl(new Supervisor3());
supervisorControl.execute(3);
}
}
}
LevelEnum
package strategy;
public enum LevelEnum {
LEVEL1, LEVEL2, LEVEL3
}
public enum LevelEnum {
LEVEL1, LEVEL2, LEVEL3
}
定义接口
package strategy;
public interface Supervisor {
void examine(int supervisorId);
}
public interface Supervisor {
void examine(int supervisorId);
}
三个Supervisor的实现类
package strategy;
public class Supervisor1 implements Supervisor {
public void examine(int supervisorId) {
System.out.println("去做一些和1级supervisor相关的工作");
}
}
public class Supervisor1 implements Supervisor {
public void examine(int supervisorId) {
System.out.println("去做一些和1级supervisor相关的工作");
}
}
package strategy;
public class Supervisor2 implements Supervisor {
public void examine(int supervisorId) {
System.out.println("去做一些和2级supervisor相关的工作");
}
}
public class Supervisor2 implements Supervisor {
public void examine(int supervisorId) {
System.out.println("去做一些和2级supervisor相关的工作");
}
}
package strategy;
public class Supervisor3 implements Supervisor {
public void examine(int supervisorId) {
System.out.println("去做一些和3级supervisor相关的工作");
}
}
public class Supervisor3 implements Supervisor {
public void examine(int supervisorId) {
System.out.println("去做一些和3级supervisor相关的工作");
}
}
SupervisorControl
package strategy;
public class SupervisorControl {
Supervisor supervisor;
public SupervisorControl(Supervisor supervisor) {
this.supervisor = supervisor;
}
public void execute(int id) {
supervisor.examine(id);
}
}
public class SupervisorControl {
Supervisor supervisor;
public SupervisorControl(Supervisor supervisor) {
this.supervisor = supervisor;
}
public void execute(int id) {
supervisor.examine(id);
}
}
还有问题
依然存在大量的if - else
混合使用策略模式和反射来解决
重新main方法
package strategy;
public class Main {
public static void main(String[] args) {
// if (LevelEnum.LEVEL1.equals(1)) {
// SupervisorControl supervisorControl = new SupervisorControl(new Supervisor1());
// supervisorControl.execute(1);
// }
// if (LevelEnum.LEVEL2.equals(2)) {
// SupervisorControl supervisorControl = new SupervisorControl(new Supervisor2());
// supervisorControl.execute(2);
// }
// if (LevelEnum.LEVEL3.equals(3)) {
// SupervisorControl supervisorControl = new SupervisorControl(new Supervisor3());
// supervisorControl.execute(3);
// }
String packageName = Supervisor1.class.getPackage().getName();
String supervisorName = packageName + "." + "Supervisor" + "2";
try {
Class<?> clazz = Class.forName(supervisorName);
Supervisor supervisor = (Supervisor) clazz.newInstance();
supervisor.examine(1);
} catch (Exception e) {
System.out.println(e);
}
}
}
public class Main {
public static void main(String[] args) {
// if (LevelEnum.LEVEL1.equals(1)) {
// SupervisorControl supervisorControl = new SupervisorControl(new Supervisor1());
// supervisorControl.execute(1);
// }
// if (LevelEnum.LEVEL2.equals(2)) {
// SupervisorControl supervisorControl = new SupervisorControl(new Supervisor2());
// supervisorControl.execute(2);
// }
// if (LevelEnum.LEVEL3.equals(3)) {
// SupervisorControl supervisorControl = new SupervisorControl(new Supervisor3());
// supervisorControl.execute(3);
// }
String packageName = Supervisor1.class.getPackage().getName();
String supervisorName = packageName + "." + "Supervisor" + "2";
try {
Class<?> clazz = Class.forName(supervisorName);
Supervisor supervisor = (Supervisor) clazz.newInstance();
supervisor.examine(1);
} catch (Exception e) {
System.out.println(e);
}
}
}
注意:
class.forName() 需要传入的格式是"包名.类名"。如果找不到包名则会报ClassNotFoundException
可以通过class.getPackage来获取包名。
class.forName() 需要传入的格式是"包名.类名"。如果找不到包名则会报ClassNotFoundException
可以通过class.getPackage来获取包名。
总结:
到此解决了if - else 很多的问题,并且解决了需要侵入代码修改的问题,
如果新增supervisor的级别,只需要和前端达成一致,后台继续写一个supervisor4对象即可。
为什么解决了test难的问题
对于test来说,每一个If-else 都需要去验证,这其实就是两个test, if 一个 else一个,如果有很多if - else,想要保证高的test覆盖率,就会非常头痛。
然而我们用策略模式 和 反射来解决,只需要,对主逻辑一个test,每一个实现方法做一个test即可。
然而我们用策略模式 和 反射来解决,只需要,对主逻辑一个test,每一个实现方法做一个test即可。
Collections 的自定义排序没有正确判断if else引发的血案
Plm 30765 这个单号查询流程报错,请协助处理分析处理一下 ,谢谢
子主题
子主题
面向对象知识与应用
面向对象的思想
面向对象的思想已经逐步取代了过程化的思想(面向过程)
面向对象与面向过程
什么是面向过程?
概述: 自顶而下的编程模式
把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。
在进行面向过程编程时,不需要考虑那么多,上来先定义一个函数,然后使用各种方式进行代码执行。
诸如if-else、for-each 等
最典型用法就是实现一个简单算法,比如实现冒泡排序。
什么是面向对象?
OOP
(Object Oriented Programming)
Java是 面向对象的高级编程语言
必须熟悉面向对象的思想才能编写出Java程序。
面向对象的编程语言主要有:C++、Java、C#等。
概述: 将事务高度抽象化的编程模式
面向对象的一个重要思想就是通过抽象得到类,即将某些数据以及针对这些数据上的操作封装在一个类中,
将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。
在进行面向对象进行编程时,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。
比如:想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car 类。
面向对象的特征
面向对象是一种常见的思想,比较符合人们的思考习惯;
面向对象可以将复杂的业务逻辑简单化,增强代码复用性;
面向对象具有抽象、封装、继承、多态等特性。
举例说明区别
同样一个象棋设计
面向对象
创建黑白双方的对象负责演算,棋盘的对象负责画布,规则的对象负责判断,
例子可以看出,面向对象更重视不重复造轮子,即创建一次,重复使用。
面向过程
开始—黑走—棋盘—判断—白走—棋盘—判断—循环。
只需要关注每一步怎么实现即可。
优劣对比
面向对象
占用资源相对高,速度相对慢。
面向过程
占用资源相对低,速度相对快
抽象的目的是从具体的实例中抽取共有属性和功能形成一种数据类型。
从抽象到类
抽象的关键是抓住事物的两个方面
属性
功能
抽象的关键有两点:
数据
数据上的操作
面向抽象编程
在设计一个程序时,可以通过在abstract类中声明若干个abstract方法,
表明这些方法在整个系统设计中的重要性,
方法体的内容细节由它的非abstract子类去完成。
使用多态进行程序设计的核心技术之一是使用上转型对象,
即将abstract类声明对象作为其子类的上转型对象,那么这个上转型对象就可以调用子类重写的方法。
所谓面向抽象编程
指当设计某种重要的类时,不让该类面向具体的类,而是面向抽象类,
即,所设计类中的重要数据是抽象类声明的对象,而不是具体类声明的对象。
开-闭原则
所谓“开-闭原则”(Open-Closed Principle)就是让设计的系统应当对扩展开放,对修改关闭。
在设计系统时,应当首先考虑到用户需求的变化,将应对用户变化的部分设计为对扩展开放,而设计的核心部分是经过精心考虑之后确定下来的基本结构,这部分应当是对修改关闭的,即不能因为用户的需求变化而再发生变化,因为这部分不是用来应对需求变化的。
如果系统的设计遵守了“开-闭原则”
那么这个系统一定是易维护的,
因为在系统中增加新的模块时,不必去修改系统中的核心模块。
案例代码
编写一个Java应用程序,该程序可以输出矩形的面积。
public class ComputerRectArea {
public static void main(String args[]) {
double height; //高
double width; //宽
double area; //面积
height=23.89;
width=108.87;
area=height*width; //计算面积
System.out.println(area);
}
}
public static void main(String args[]) {
double height; //高
double width; //宽
double area; //面积
height=23.89;
width=108.87;
area=height*width; //计算面积
System.out.println(area);
}
}
如果其他Java应用程序也想计算矩形的面积,同样需要知道使用矩形的宽和高来计算矩形面积的算法,即也需要编写和这里同样多的代码。
现在提出如下问题:能否将和矩形有关的数据以及计算矩形面积的代码进行封装,使得需要计算矩形面积的Java应用程序的主类无需编写计算面积的代码就可以计算出矩形的面积呢?
简单的矩形类
对所观察的矩形做如下抽象:
●矩形具有宽和高之属性。
●可以使用矩形的宽和高计算出矩形的面积。
●矩形具有宽和高之属性。
●可以使用矩形的宽和高计算出矩形的面积。
现在根据如上的抽象,编写出如下的Rect类(Rect.java)
public class Rect
{
double width; //矩形的宽
double height; //矩形的高
double getArea() //计算面积的方法
{
double area=width*height;
return area;
}
}
{
double width; //矩形的宽
double height; //矩形的高
double getArea() //计算面积的方法
{
double area=width*height;
return area;
}
}
面向对象的优点
面向对象优点代码开发模块化,更易维护和修改。代码复用增强代码的可靠性和灵活性。增加代码的可理解性。
代码开发模块化,更易维护和修改。
代码复用
增强代码的可靠性和灵活性。
增加代码的可理解性。
面向对象的三大基本特征
封装
什么叫封装
封装(Encapsulation)
把客观事物,封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
封装是面向对象的特征之一,是对象和类概念的主要特性
一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。
在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。
通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
封装给对象提供了隐藏内部特性和行为的能力。
对象提供一些能被其他对象访问的方法来改变它内部的数据。
封装与访问权限
访问控制权限,又称为封装
所谓访问权限,是指对象是否可以通过“.”运算符操作自己的变量或通过“.”运算符使用类中的方法。
访问控制权限其实最核心就是一点:只对需要的类可见。
当用一个类创建了一个对象之后,该对象可以通过“.”运算符操作自己的变量、使用类中的方法,
但对象操作自己的变量和使用类中的方法是有一定限制的。
如果你信任的下属 对你隐瞒bug,你是根本不知道的。
好处
通过隐藏对象的属性来保护对象内部的状态
提高了代码的可用性和可维护性
因为对象的行为可以被单独的改变或者是扩展
禁止对象之间的不良交互,提高模块化。
Java中成员的访问权限共有四种
public、protected、default、private
访问限制修饰符都是Java的关键字,用来修饰成员变量或方法。
可见性如下
Java中成员的访问权限与可见性
私有变量和私有方法
用关键字private修饰的成员变量和方法
只有在本类中创建该类的对象时,这个对象才能访问自己的私有成员变量和类中的私有方法。
class Tom {
private float weight;
private float f(float a,float b){
return a+b;
}
}
private float weight;
private float f(float a,float b){
return a+b;
}
}
class Jerry {
void g() {
Tom cat=new Tom();
cat.weight=23f; //非法
float sum=cat.f(3,4); //非法
}
}
void g() {
Tom cat=new Tom();
cat.weight=23f; //非法
float sum=cat.f(3,4); //非法
}
}
当用某个类在另外一个类中创建对象后,如不希望该对象直接访问自己的变量,就应当将该成员变量访问权限设置为private。
直接访问自己的变量指的是通过“.”运算符来操作自己的成员变量
面向对象编程提倡对象应当调用方法来改变自己的属性,类应当提供操作数据的方法,这些方法可以经过精心的设计,使得对数据的操作更加合理
共有变量和共有方法
用public修饰的成员变量和方法
在任何一个类中用类Tom 创建了一个对象后,该对象能访问自己的public变量和类中的public方法(也可以通过类名来操作成员变量、方法)
class Tom {
public float weight;
public float f(float a,float b) {
return a+b;
}
}
public float weight;
public float f(float a,float b) {
return a+b;
}
}
class Jerry {
void g() {
Tom cat=new Tom();
cat.weight=23f; //合法
float sum=cat.f(3,4); //合法
}
}
void g() {
Tom cat=new Tom();
cat.weight=23f; //合法
float sum=cat.f(3,4); //合法
}
}
友好变量和友好方法(default)
当在另外一个类中用类Tom 创建了一个对象后,如果这个类与Tom类在同一个包中,那么该对象能访问自己的友好变量和友好方法。
在任何一个与Tom同一包中的类中,也可以通过Tom类的类名访问Tom类的类友好成员变量和类友好方法。
class Tom {
float weight;
float f(float a,float b) {
return a+b;
}
}
float weight;
float f(float a,float b) {
return a+b;
}
}
class Jerry {
void g() {
Tom cat=new Tom();
cat.weight=23f; //合法
float sum=cat.f(3,4); //合法
}
}
void g() {
Tom cat=new Tom();
cat.weight=23f; //合法
float sum=cat.f(3,4); //合法
}
}
受保护的成员变量和方法
用protected修饰的成员变量和方法被称为受保护的成员变量和受保护的方法 。
class Tom {
protected float weight;
protected float f(float a,float b) {
return a+b;
}
}
protected float weight;
protected float f(float a,float b) {
return a+b;
}
}
class Jerry {
void g() {
Tom cat=new Tom();
cat.weight=23f; //合法
float sum=cat.f(3,4); //合法
}
}
void g() {
Tom cat=new Tom();
cat.weight=23f; //合法
float sum=cat.f(3,4); //合法
}
}
protected的进一步说明
一个类A中的protected成员变量和方法可以被它的直接子类和间接子类继承
如B是A的子类,C是B的子类,D又是C的子类,那么B、C和D类都继承了A类的protected成员变量和方法。
如果用D类在D中创建了一个对象,该对象总是可以通过“.”运算符访问继承的或自己定义的protected变量和protected方法的,但是,如果在另外一个类中,如在Other类中用D类创建了一个对象object,该对象通过“.”运算符访问protected变量和protected方法的权限如所述。
(1)对于子类D中声明的protected成员变量和方法,如果object要访问这些protected成员变量和方法,只要Other类和D类在同一个包中就可以了。
(2)如果子类D的对象的protected成员变量或protected方法是从父类继承的,那么就要一直追溯到该protected成员变量或方法的“祖先”类,即A类,如果Other类和A类在同一个包中,那么object对象能访问继承的protected变量和protected方法。
Java中的修饰符
Java有3种修饰符
public,private和protected
每一种修饰符给其他的位于同一个包或者不同包下面对象赋予了不同的访问权限。
public类与友好类
public 类
类声明时,如果在关键字class前面加上public关键字,就称这样的类是一个public 类 。
可以在任何另外一个类中,使用public类创建对象。
友好类
如果一个类不加public修饰,这样的类被称作友好类。
在另外一个类中使用友好类创建对象时,要保证它们是在同一包中。
继承
继承是什么?
继承(Inheritance)
一种联结类与类的层次模型
可使用现有类的所有功能,并在无需重新编写原来的类的情况下,对这些功能进行扩展。
继承提供了代码的重用行,也可以在不修改类的情况下给现存的类添加新特性。
继承给对象提供了从基类获取字段和方法的能力。
继承是一种is-a 关系
继承是一种is-a 关系。
所有OOP语言和Java语言都不可或缺的一部分。
继承的过程,就是从一般到特殊的过程。
继承可以帮助我实现类的复用,但长期大量的使用继承会给代码带来很高的维护成本。
继承是类与类或者接口与接口之间最常见的关系
子类与父类
利用继承,可以先编写一个共有属性的一般类,
指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力
通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
根据该一般类,再编写具有特殊属性的新类,新类继承一般类的状态和行为,并根据需要增加它自己的新的状态和行为。
由继承而得到的类称为子类,被继承的类称为父类(超类)。
题目
Java中 , 如果类C是类B的子类,类B是类A的子类,那么一下描述真确的是
A C类不仅继承了B中的成员,同样也继承了A中的成员
B C只继承了B中的成员
C C只继承了A中的成员
D C不能继承A或B中的成员
A C类不仅继承了B中的成员,同样也继承了A中的成员
B C只继承了B中的成员
C C只继承了A中的成员
D C不能继承A或B中的成员
选A
向上转型和向下转型
向上转型代表了父类与子类之间的关系,其实父类和子类之间不仅仅有向上转型,还有向下转型,它们 的转型后的范围不一样
向上转型
通过子类对象(小范围)转化为父类对象(大范围)
这种转换是自动完成的,不用强制。
向下转型
通过父类对象(大范围)实例化子类对象(小范围)
这种转换不是自动完成的,需要强制 指定。
对象的上转型对象
假设,A类是B类的父类,当用子类创建一个对象,并把这个对象的引用放到父类的对象中时,比如:
A a;
a=new B();
或
A a;
B b=new B();
a=b;
a=new B();
或
A a;
B b=new B();
a=b;
对象的上转型对象
注:
① 不要将父类创建的对象和子类对象的上转型对象混淆。
② 可以将对象的上转型对象再强制转换到一个子类对象,这时,该子类对象又具备了子类所有属性和功能。
③ 不可以将父类创建的对象的引用赋值给子类声明的对象
① 不要将父类创建的对象和子类对象的上转型对象混淆。
② 可以将对象的上转型对象再强制转换到一个子类对象,这时,该子类对象又具备了子类所有属性和功能。
③ 不可以将父类创建的对象的引用赋值给子类声明的对象
父子类与多态是什么?
继承概念的实现方式有二类
实现继承
指直接使用基类的属性和方法而无需额外编码的能力
接口继承
指仅使用属性和方法的名称、但是子类必须提供实现的能力。
继承的关键字是extends
只要创建了一个类,就隐式的继承自Object父类,只不过没有指定。
如果显示指定了父类,那么继承于父类,而父类继承于Object类。
图解继承
图解继承
如果使用了 extends显示指定了继承,那么我们可以•说 Father是父类,而Son是子类
用代码表示如下
用代码表示如下
继承双方拥有某种共性的特征
继承双方拥有某种共性的特征
如果Son没有实现自己的方法,那么默认就是用的是父类的feature方法。
如果子类实现了自己的feature方法,那么就相当于是重写了父类的feature方法,这也是重写
声明一个类的子类的格式如下
class 子类名 extends 父类名 {
… …
}
… …
}
例如:
class Student extends People {
… …
}
class Student extends People {
… …
}
子类的继承性
所谓子类继承父类的成员变量作为自己的一个成员变量,就好象它们是在子类中直接声明一样,可以被子类中自己定义的任何实例方法操作。
所谓子类继承父类的方法作为子类中的一个方法,就象它们是在子类中直接定义了一样,可以被子类中自己定义的任何实例方法调用。
子类和父类在同一包中的继承性
如果子类和父类在同一个包中,那么,
子类自然地继承了其父类中不是private的成员变量,作为自己的成员变量,
并且也自然地继承了父类中不是private的方法作为自己的方法,
继承的成员变量或方法的访问权限保持不变。
子类和父类不在同一包中的继承性
如果子类和父类不在同一个包中,那么,
子类继承了父类的protected、public成员变量,作为子类的成员变量,
并且继承了父类的protected、public方法为子类的方法,
继承的成员或方法的访问权限保持不变。
子类对象的特点
当用子类的构造方法创建一个子类的对象时,不仅子类中声明的成员变量被分配了内存,而且父类的成员变量也都分配了内存空间,但只将其中一部分作为分配给子类对象的变量。也就是说,父类中的private成员变量尽管分配了内存空间,也不作为子类对象的变量,即子类不继承父类的私有成员变量。同样,如果子类和父类不在同一包中,尽管父类的友好成员变量分配了内存空间,但也不作为子类的成员变量,即如果子类和父类不在同一包中,子类不继承父类的友好成员变量.
Java 的继承与实现
继承
都体现了传递性
使用extends 关键字实现
继承的根本原因
因为要复用
如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类
在继承中可以定义属性方法,变量,常量等。
实现
都体现了传递性
实现通过implements 关键字。
实现的根本原因
需要定义一个标准
如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准
让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标。
Java 中支持一个类同时实现多个接口,但是不支持同时继承多个类。
在接口中只能定义全局常量(static final)和无实现的方法(Java 8 以后可以有defult 方法)
翻译
就是同样是一台汽车,既可以是电动车,也可以是汽油车,也可以是油电混合的,
只要实现不同的标准就行了,但是一台车只能属于一个品牌,一个厂商。
class Car extends Benz implements GasolineCar, ElectroCar{
}
}
Java支持多继承么?
不支持,Java不支持多继承。每个类都只能继承一个类,但是可以实现多个接口。
多态
什么是多态
多态(Polymorphism)
多态的基础
封装
继承
多态机制使具有不同内部结构的对象可以共享相同的外部接口
虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
最常见的多态就是将子类传入父类参数中,运行时调用父类方法时通过传入的子类决定具体的内部结构或行为。
多态是编程语言给不同的底层数据类型做相同的接口展示的一种能力。
一个多态类型上的操作可以应用到其他类型的值上面。
多态性就是指父类的某个实例方法被其子类重写时,可以各自产生自己的功能行为。
继承与多态
当一个类有很多子类时,并且这些子类都重写了父类中的某个实例方法,那么当把子类创建的对象的引用放到一个父类的对象中时,就得到了该对象的一个上转型对象,那么这个上转型对象在调用这个实例方法时就可能具有多种形态,因为不同的子类在重写父类的实例方法时可能产生不同的行为。
如何实现多态?多态的实现具有三种充要条件
多态的必要条件
为了实现运行期的多态,或者说是动态绑定,需要满足三个条件。
图解多态的必要条件
图解多态的必要条件
这样,就实现了多态,同样是Parent 类的实例,p.call 调用的是Son 类的实现、p1.call 调用的是Daughter 的实现。
有人说,你自己定义的时候不就已经知道p 是son,p1 是Daughter 了么。但是,有些时候你用到的对象并不都是自己声明的啊。
比如Spring 中的IOC 出来的对象,你在使用的时候就不知道他是谁,或者说你可以不用关心他是谁。根据具体情况而定。
继承
有类继承或者接口实现
重写父类方法
子类要重写父类的方法
父类引用指向子类对象
多态还分为
动态多态
多态是一种运行期的状态。
多态应该是一种运行期特性,Java 中的重写是多态的体现。
上面提到的那种动态绑定认为是动态多态,因为只有在运行期才能知道真正调用的是哪个类的方法。
重载和多态其实是无关的
静态多态
一般认为Java 中的函数重载是一种静态多态,因为需要在编译期决定具体调用哪个方法。
因为在编译期已经确定调用哪个方法,所以重载并不是多态。
(注:严格来说,重载是编译时多态,即静态多态)
重载和重写就是多态的具体表现形式。
比如下面这段代码
比如下面这段代码
main方法中有一个很神奇的地方,Fruit fruit = new Apple。
Fruit类型的对象竟然指向了 Apple对象的引用,这其实就是多态-> 父类引用指向子类对象,
因为Apple继承于 Fruit,并且重写了 eat方法,所以能够表现出来多种状态的形式。
接口和抽象类
接口
接口是什么?
Java语言中,接口是由interface关键字来表示的,
接口相当于就是对外的一种约定和标准
接口诞生的背景
Java不支持多继承性,即一个类只能有一个父类。单继承性使得Java简单,易于管理和维护
为了克服Java单继承的缺点,Java使用了接口,一个类可以实现多个接口。
接口类比于操作系统
拿操作系统举例子,为什么会有操作系统?
就会为了屏蔽 软件的复杂性和硬件的简单性之间的差异,为软件提供统一的标准。
如何定义一个接口?
定义一个接口
比如定义了一个CxuanGoodJob的接口,然后你就可以在其内部定义cxuan做的好的那些事情, 比如cxuan写的文章不错。
接口特征
interface接口是一个完全抽象的类,他不会提供任何方法的实现,只是会进行方法的定义。
接口中只能使用两种访问修饰符
一种是public,它对整个项目可见;
一种是default缺省值,它只具有包访问权限。
接口只提供方法的定义,接口没有实现,但是接口可以被其他类实现。
子主题
实现接口的类需 要提供方法的实现,实现接口使用implements关键字来表示,一个接口可以有多个实现。
接口不能被实例化,所以接口中不能有任何构造方法,你定义构造方法编译会出错。
接口的实现必须实现接口的全部方法,否则必须定义为抽象类,
接口的声明与使用
使用关键字interface来定义一个接口。
接口的定义和类的定义很相似,分为接口的声明和接口体。
1.接口声明
接口通过使用关键字interface来声明
格式:interface 接口的名字
2.接口体
接口体中包含常量定义和方法定义两部分。
3.接口的使用
类通过使用关键字implements声明自己实现一个或多个接口如果实现多个接口,则用逗号隔开接口名.
class A implements Printable,Addable
class Dog extends Animal implements Eatable,Sleepable
class Dog extends Animal implements Eatable,Sleepable
如果一个类实现了某个接口,那么这个类必须重写该接口的所有方法。
如果一个类实现了某个接口,那么这个类必须重写该接口的所有方法。
例如, import java.io.*;
理解接口
接口可以增加很多类都需要具有的功能,不同的类可以实现相同的接口,同一个类也可以实现多个接口。
接口只关心操作,并不关心操作的具体实现
接口的思想:在于它可以增加很多类都需要具有的功能,而且实现相同的接口类不一定有继承关系。
接口的UML图
表示接口的UML图和表示类的UML图类似,使用一个长方形描述一个接口的主要构成,将长方形垂直地分为三层。
第1层是名字层、第2层是常量层、第3层是方法层 。
接口UML图
实现关系的UML图
接口回调
接口变量与回调机制
空接口的内存模型
对象调用方法的内存模型
接口回调是指
可以把实现某一接口的类创建的对象的引用赋给该接口声明的接口变量中,那么该接口变量就可以调用被类重写的接口方法。
实际上,当接口变量调用被类重写的接口方法时,就是通知相应的对象调用这个方法。
接口回调用接口方法的内存模型
接口的多态性
把实现接口的类的实例的引用赋值给接口变量后,该接口变量就可以回调类重写的接口方法。
由接口产生的多态就是指不同的类在实现同一个接口时可能具有不同的实现方式,那么接口变量在回调接口方法时就可能具有多种形态。
面向接口编程
面向接口去设计程序,可以通过在接口中声明若干个abstract方法,表明这些方法的重要性,方法体的内容细节由实现接口的类去完成。
使用接口进行程序设计的核心思想是使用接口回调,即接口变量存放实现该接口的类的对象的引用,从而接口变量就可以回调类实现的接口方法。
接口可体现程序设计的“开-闭”原则
对扩展开放,对修改关闭。
UML类图
抽象类
抽象
抽象是把想法从具体的实例中分离出来的步骤
因此,要根据他们的功能,而不是实现细节来创建类。
Java支持创建只暴漏接口而不包含方法实现的抽象的类。
这种抽象技术的主要目的是把类的行为和实现细节分离开。
抽象类是一种抽象能力弱于接口的类,
接口、抽象类与实现类之间的关系
如果把接口形容为狗这个物种
那么抽象类可以说是毛发是白色、小体的品种
而实现类可以是具体的类,比如 说是博美、泰迪等。
抽象类中,具有如下特征
如果一个类中有抽象方法,那么这个类一定是抽象类
使用关键字abstract修饰的 方法一定是抽象方法,具有抽象方法的类一定是抽象类。实现类方法中只有方法具体的实现。
抽象类中不一定只有抽象方法,抽象类中也可以有具体的方法,你可以自己去选择是否实现这些方 法。
抽象类中的约束不像接口那么严格,你可以在抽象类中定义构造方法、抽象方法、普通属性、方法、静态属性和静态方法
抽象类和接口一样不能被实例化,实例化只能实例化具体的类
abstract类与接口的比较
接口和abstract类的比较如下:
1.abstract类和接口都可以有abstract方法。
2.接口中只可以有常量,不能有变量;而abstract类中即可以有常量也可以有变量。
3.abstract类中也可以有非abstract方法,接口不可以。
abstract类
用关键字abstract修饰的类称为abstract类(抽象类)。
在Java中,抽象类使用abstract关键字来表示。
abstract类不能用new运算创建对象
你可以像下面这样定义抽象类
如:
abstract class A {
…
}
abstract class A {
…
}
定义抽象类
abstract()方法
用关键字abstract修饰的方法称为abstract方法(抽象方法)
例如:
abstract int min(int x,int y);
abstract int min(int x,int y);
abstract类中可以有abstract方法
常见问题
接口和抽象类的区别是什么?
Java提供和支持创建抽象类和接口,它们的实现有共同点
都不能实例化
不同点在于:
可以参考JDK8中抽象类和接口的区别
接口
所有的方法隐含的都是抽象的
类如果要实现一个接口,它必须要实现接口声明的所有方法。
Java接口中声明的变量默认都是final的。
接口内不能有构造方法
接口是绝对抽象的,不可以被实例化。
访问修饰符
Java接口中的成员函数默认是public的。
接口方法是public,default
数量
类可以实现很多个接口,但是只能继承一个抽象类
一个子类只能继承一个抽象类,可以实现多个接口。
抽象类
可同时包含抽象和非抽象的方法
类可以不实现抽象类声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
抽象类可以在不提供接口方法实现的情况下,实现接口。
抽象类可以包含非final的变量。
抽象类可以有构造方法
抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。
数量
一个子类只能继承一个抽象类,可以实现多个接口。
类可以实现很多个接口,但是只能继承一个抽象类
访问修饰符
抽象类中的方法是public,private,protected,default
抽象类的成员函数可以是private,protected或者是public。
抽象和封装的不同点
抽象和封装是互补的概念。
一方面,抽象关注对象的行为。
另一方面,封装关注对象行为的细节。
一般是通过隐藏对象内部状态信息做到封装
因此,封装可以看成是用来提供抽象的一种策略。
面向对象的复用技术
复用性
面向对象技术带来的很棒的潜在好处之一
如果运用的好的话,可以帮助我们节省很多开发时间,提升开发效率。
如果被滥用那么就可能产生很多难以维护的代码。
作为一门面向对象开发的语言,代码复用是Java 引人注意的功能之一
Java 代码的复用有三种具体的表现形式
继承
继承复用
对象的组合
什么是组合
一个类的成员变量可以是Java允许的任何数据类型
一个类可以把对象作为自己的成员变量,如果用这样的类创建对象,那么该对象中就会有其他对象
也就是说该对象将其他对象作为自己的组成部分 。
组合其实不难理解,就是将对象引用置于新类中即可。
组合也是一种提高类的复用性的一种方式。
如果 你想让类具有更多的扩展功能,你需要记住一句话多用组合,少用继承。
组合(Composition)体现的是整体与部分、拥有的关系,即has-a 的关系。
举例
代码中SoccerPlayer引用了 Soccer类,通过引用Soccer类,来达到调用soccer中的属性和方法。
组合与继承的区别和联系
一般情况下,组合和继承 也是一对可以连用的好兄弟。
关于继承和组合孰优孰劣的争论没有结果,只要发挥各自的长处和优点即可,
继承的代码复用是一种白盒式代码复用
在继承结构中,父类的内部细节对于子类是可见的。
通常也可以说通过继承的代码复用是一种白盒式代码复用。
(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性。)
组合的代码复用是一种黑盒式代码复用
通过对现有的对象进行拼装(组合)产生新的、更复杂的功能
因为在对象之间,各自的内部细节是不可见的
所以我们也说这种方式的代码复用是黑盒式代码复用。
(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)
继承在编译期就确定了关系
在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。
(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)
类的组合关系一般在运行期确定
组合,在写代码的时候可以采用面向接口编程
所以,类的组合关系一般在运行期确定。
优缺点对比
组合和继承是有区别的
子主题
子主题
组合是has-a 的关系
组合(Composition)体现的是整体与部分、拥有的关系,即has-a 的关系。
如何选择
相信很多人都知道面向对象中有一个比较重要的原则『多用组合、少用继承』或者说『组合优于继承』。
从前面的介绍已经优缺点对比中也可以看出,组合确实比继承更加灵活,也更有助于代码维护。
建议在同样可行的情况下,优先使用组合而不是继承。因为组合更安全,更简单,更灵活,更高效。
继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一
问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该
好好考虑是否需要继承。《Java 编程思想》
问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该
好好考虑是否需要继承。《Java 编程思想》
只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A 和B,
只有当两者之间确实存在is-a 关系的时候,类B 才应该继承类A。《Effective Java》
只有当两者之间确实存在is-a 关系的时候,类B 才应该继承类A。《Effective Java》
代理
除了继承和组合外,另外一种值得探讨的关系模型称为代理。
代理的大致描述是,A想要调用B类 的方法,A不直接调用,A会在自己的类中创建一个B对象的代理,再由代理调用B的方法。
例如
代理
面向对象的五大基本原则
单一职责原则(Single-Responsibility Principle)
核心思想
一个类,最好只做一件事,只有一个引起它的变化。
是什么?
可以看做是低耦合、高内聚在面向对象原则上的引申
将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
单一职责
通常意义下的单一职责,就是指只有一种单一功能,
不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
专注,是一个人优良的品质;同样的,单一也是一个类的优良设计。
交杂不清的职责将使得代码看起来特别别扭牵一发而动全身,有失美感和必然导致丑陋的系统错误风险。
职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。
开放封闭原则(Open-Closed principle)
核心思想
软件实体应该是可扩展的,而不可修改的
对扩展开放,对修改封闭的。
主要体现在两个方面
1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
2、对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
实现开放封闭原则的核心思想
对抽象编程,而不对具体编程,因为抽象相对稳定。
让类依赖于固定的抽象,所以修改就是封闭的;
而通过面向对象的继承和多态机制,又可实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。
用封闭开放原则来封闭变化满足需求
“需求总是变化”没有不变的软件
所以就需要用封闭开放原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。
Liskov 替换原则(Liskov-Substitution Principle)
核心思想
子类必须能够替换其基类
这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。
同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
Liskov 替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循Liskov 替换原则,才能保证继承复用是可靠地。
实现的方法
是面向接口编程
通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责
将公共部分抽象为基类接口或抽象类
Liskov替换原则是关于继承机制的设计原则,违反了Liskov 替换原则,就必然导致违反开放封闭原则。
好处
Liskov 替换原则能够保证系统具有良好的拓展性,
同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。
依赖倒置原则(Dependecy-Inversion Principle)
核心思想
依赖于抽象。
高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
我们知道,依赖一定会存在于类与类、模块与模块之间。
当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:
在依赖之间定义一个抽象的接口
使得高层模块调用接口,
而底层模块实现接口的定义
以此来有效控制耦合关系,达到依赖于抽象的设计目标。
抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。
依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一层不变的。
依赖于抽象,就是对接口编程,不要对实现编程。
接口隔离原则(Interface-Segregation Principle)
核心思想
使用多个小的专门的接口,而不要使用一个大的总接口。
接口隔离原则体现在:
接口应该是内聚的,应该避免“胖”接口。
接口污染
一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。
接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单
一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;
而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这
会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种
灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们
的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。
一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;
而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这
会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种
灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们
的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。
分离的手段主要有以下两种
1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。
2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。
以上就是5个基本的面向对象设计原则
它们就像面向对象程序设计中的金科玉律,遵守它们可以使我们的代码更加鲜活,易于复用,易于拓展,灵活优雅。
不同的设计模式对应不同的需求,而设计原则则代表永恒的灵魂,需要在实践中时时刻刻地遵守。
就如ARTHUR J.RIEL 在那边《OOD 启示录》中所说的:
“你并不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。
但你应当把这些原则看做警铃,若违背了其中的一条,那么警铃就会响起。”
类与对象的关系
什么是类?
一种数据类型
类是Java语言中最重要的一种数据类型。
类也是一种数据类型,因此可以使用类来声明一个变量。
类是体现封装的一种数据类型
类是面向对象语言中最重用的一种数据类型,那么就可以用它来声明变量。
大多数面向对象的语言都使用class来定义类
就比如书籍一样,类相当于是书的封面
类是组成Java程序的基本要素。
类与对象的关系
类也是一种对象,相当于是一系列对象的抽象
通俗的讲,类是创建和用来定义对象的“模板”,没有类就没有对象。
类的作用
它告诉你它里面定义的对象都是什么样的
类封装了一类对象的状态和方法。
当使用一个类创建一个对象时,也称给出了这个类的一个实例。
在面向对象语言(如Java)中,用类声明的变量就称之为一个对象。类声明的变量称做对象
和基本数据类型不同,在用类声明对象后,还必须要创建对象,即为声明的对象分配变量(确定对象所具有的属性)
定义类
上面,声明了一个class类,现在,可 以使用new来创建这个对象
ClassName classname = new ClassName();
类的实现包括两部分
类声明
例如:
class Vehicle {
……
}
class Vehicle {
……
}
“class Vehicle”称作类声明;
“Vehicle”是类名。
class 类名
类体
类声明之后的一对大括号“{”,“}”以及它们之间的内容称作类体,大括号之间的内容称作类体的内容。
类体的内容由两部分构成
变量的声明
刻画属性
方法的定义
刻画功能
一般使用下面来定义类,基本格式如下
一般使用下面来定义类
class 类名 {
类体的内容
}
类体的内容
}
—般,类的命名遵循驼峰原则,驼峰原则的定义
骆驼式命名法(Came卜Case)又称驼峰式命名法,
是电脑程式编写时的一套命名规则(惯例)。
正如它的名称CamelCase所表示的那样,是指混合使用大小写字母来构成变量和函数的名字。
程序员们为了自己的代码能更容易的在同行之间交流,所以多采取统一的可读性比较好的命名方 式。
给类命名时,遵守下列编程风格
这不是语法要求的,但应当遵守
1.如果类名使用拉丁字母,那么名字的首字母使用大写字母,如。
2、类名最好容易识别、见名知意
当类名由几个“单词”复合而成的,每个单词的首字母使用大写
对象的创建与使用
用类创建对象需经过两个步骤
声明对象
(1)声明对象时的内存模型
Vehicle car1 ;
(1)声明对象时的内存模型
Rect rectangle1;
rectangle1在内存中还没有任何数据
一般格式为: 类的名字 对象名字
如:Vehicle car;
Vehicle是一个类的名字,
car是我们声明的对象的名字。
声明对象的案例
例如用Rect声明一个名字为rectangle1的对象的代码如下:
声明对象变量rectangle1后,rectangle1在内存中还没有任何数据,称这时的rectangle1是一个空对象。
空对象不能使用,必须再进行为对象分配变量的步骤。
为对象分配(成员)变量
为对象分配变量
在Java中,一旦创建了一个引用,就希望它能与一个新的对象进行关联
使用new运算符和类的构造方法为声明的对象分配变量,即创建对象。
通常使用new操作符来实现这一目的。
new的意思,绐我一个新对象
为声明的对象分配内存
(2)对象分配内存后的内存模型
car1 = new Vehicle();
(2)对象分配内存后的内存模型
为上述Rect类声明的rectangle1对象分配内存的代码如下:
rectangle1 = new Rect();
为对象分配变量后的内存模型
在声明对象时,可以同时为对象分配变量,例如,
Rect rectangle1 = new Rect();
如果类中没有构造方法,系统会调用默认的构造方法,默认的构造方法是无参数的,且方法体中没有语句。
创建多个不同的对象
一个类可以创建多个不同的对象,这些对象将被分配不同的变量,改变其中一个对象的状态不会影响其他对象的状态。
创建多个对象的内存模型
Rect rectangle1, rectangle2;
rectangle1 = new Rect();
rectangle2 = new Rect();
rectangle1 = new Rect();
rectangle2 = new Rect();
创建多个对象的内存模型
创建多个不同的对象
使用对象
对象通过使用“.”运算符,操作自己的变量和调用方法
通过使用运算符“.”,对象,可以实现对自己变量的访问和方法的调用。
对象不仅可以操作自己的变量改变状态,而且能调用类中的方法产生一定的行为。
1.对象操作自己的变量(对象的属性)
对象.变量;
调用方法的格式为: 对象.变量;
例如,
rectangle1.width=12;
rectangle1.height=9;
rectangle1.width=12;
rectangle1.height=9;
2.对象调用类中的方法(对象的功能)
对象.方法;
调用方法的格式为: 对象.方法;
例如,
rectangle1.getArea();
rectangle1.getArea();
3.体现封装
当对象调用方法时,方法中出现的成员变量就是指分配给该对象的变量。
对象体现封装
代码:
car1.setPower(128);
car2.setPower(76);
car1.speedUp(80);
car2.speedUp(100);
car1.setPower(128);
car2.setPower(76);
car1.speedUp(80);
car2.speedUp(100);
子主题
对象的案例:使用矩形对象
Rect.java
public class Rect
{
double width; //矩形的宽
double height; //矩形的高
double getArea() //计算面积的方法
{
double area=width*height;
return area;
}
}
{
double width; //矩形的宽
double height; //矩形的高
double getArea() //计算面积的方法
{
double area=width*height;
return area;
}
}
Example2_1.java
public class Example2_1
{
public static void main(String args[])
{
Rect rectangle1,rectangle2;
rectangle1 = new Rect();
rectangle2 = new Rect();
rectangle1.width=128;
rectangle1.height=69;
rectangle2.width=18.9;
rectangle2.height=59.8;
double area=rectangle1.getArea();
System.out.println("rectangle1的面积:"+area);
area=rectangle2.getArea();
System.out.println("rectangle2的面积:"+area);
}
}
{
public static void main(String args[])
{
Rect rectangle1,rectangle2;
rectangle1 = new Rect();
rectangle2 = new Rect();
rectangle1.width=128;
rectangle1.height=69;
rectangle2.width=18.9;
rectangle2.height=59.8;
double area=rectangle1.getArea();
System.out.println("rectangle1的面积:"+area);
area=rectangle2.getArea();
System.out.println("rectangle2的面积:"+area);
}
}
在Java应用程序中使用矩形对象
四种显式地创建对象的方式
1.用new语句创建对象,这是最常用的创建对象的方式。
2.运用反射手段,调用Java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。
3.调用对象的clone()方法。
4.运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法.
演示
下面演示了用前面3种方式创建对象的过程。
public class Customer implements Cloneable{
private String name;
private int age;
public Customer(){
this("unknown",0);
System.out.println("call default constructor");
}
public Customer(String name,int age){
this.name=name;
this.age=age;
System.out.println("call second constructor");
}
public Object clone()throws CloneNotSupportedException{
return super.clone();
}
public boolean equals(Object o){
if(this==o)
return true;
if(! (o instanceof Customer))
return false;
final Customer other=(Customer)o;
if(this.name.equals(other.name) && this.age==other.age)
return true;
else
return false;
}
public String toString(){
return "name="+name+",age="+age;
}
public static void main(String args[])throws Exception{
//运用反射手段创建Customer对象
Class objClass=Class.forName("Customer");
Customer c1=(Customer)objClass.newInstance(); //会调用Customer类的默认构造方法
System.out.println("c1: "+c1); //打印name=unknown,age=0
//用new语句创建Customer对象
Customer c2=new Customer("Tom",20);
System.out.println("c2: "+c2); //打印name=tom,age=20
//运用克隆手段创建Customer对象
Customer c3=(Customer)c2.clone(); //不会调用Customer类的构造方法
System.out.println("c2==c3 : "+(c2==c3)); //打印false
System.out.println("c2.equals(c3) : "+c2.equals(c3)); //打印true
System.out.println("c3: "+c3); //打印name=tom,age=20
}
}
private String name;
private int age;
public Customer(){
this("unknown",0);
System.out.println("call default constructor");
}
public Customer(String name,int age){
this.name=name;
this.age=age;
System.out.println("call second constructor");
}
public Object clone()throws CloneNotSupportedException{
return super.clone();
}
public boolean equals(Object o){
if(this==o)
return true;
if(! (o instanceof Customer))
return false;
final Customer other=(Customer)o;
if(this.name.equals(other.name) && this.age==other.age)
return true;
else
return false;
}
public String toString(){
return "name="+name+",age="+age;
}
public static void main(String args[])throws Exception{
//运用反射手段创建Customer对象
Class objClass=Class.forName("Customer");
Customer c1=(Customer)objClass.newInstance(); //会调用Customer类的默认构造方法
System.out.println("c1: "+c1); //打印name=unknown,age=0
//用new语句创建Customer对象
Customer c2=new Customer("Tom",20);
System.out.println("c2: "+c2); //打印name=tom,age=20
//运用克隆手段创建Customer对象
Customer c3=(Customer)c2.clone(); //不会调用Customer类的构造方法
System.out.println("c2==c3 : "+(c2==c3)); //打印false
System.out.println("c2.equals(c3) : "+c2.equals(c3)); //打印true
System.out.println("c3: "+c3); //打印name=tom,age=20
}
}
以上程序的打印结果如下:
call second constructor
call default constructor
c1: name=unknown,age=0
call second constructor
c2: name=Tom,age=20
c2==c3 : false
c2.equals(c3) : true
c3: name=Tom,age=20
call default constructor
c1: name=unknown,age=0
call second constructor
c2: name=Tom,age=20
c2==c3 : false
c2.equals(c3) : true
c3: name=Tom,age=20
从以上打印结果看出,
用方式1或方式2创建Customer对象时,都会执行Customer类的构造方法
使用方式2创建Customer对象时,不会执行Customer类的构造方法。(区别)
隐含地创建对象
1.java命令中的每个命令行参数传入程序入口main(String args[])方法
对于java命令中的每个命令行参数,Java虚拟机都会创建相应的String对象,
并把它们组织到一个String数组中,再把该数组作为参数传给程序入口main(String args[])方法。
2.程序代码中的String类型的直接数对应一个String对象
String s1="Hello";
String s2="Hello"; //s2和s1引用同一个String对象
String s3=new String("Hello");
System.out.println(s1==s2); //打印true
System.out.println(s1==s3); //打印false
String s2="Hello"; //s2和s1引用同一个String对象
String s3=new String("Hello");
System.out.println(s1==s2); //打印true
System.out.println(s1==s3); //打印false
执行完以上程序,内存中实际上只有两个String对象
一个是直接数,由Java虚拟机隐含地创建,
一个通过new语句显式地创建。
3.字符串操作符“+”的运算结果为一个新的String对象
String s1="H";
String s2=" ello";
String s3=s1+s2; //s3引用一个新的String对象
System.out.println(s3=="Hello"); //打印false
System.out.println(s3.equals("Hello")); //打印true
String s2=" ello";
String s3=s1+s2; //s3引用一个新的String对象
System.out.println(s3=="Hello"); //打印false
System.out.println(s3.equals("Hello")); //打印true
4.当Java虚拟机加载一个类时,会隐含地创建描述这个类的Class实例.
对象的引用和实体
对象引用与对象之间的关系,
对象的引用(reference)
在Java中,万事万物都是对象
尽管一切都看作是对象,但是你操纵的却 是一个对象的引用(reference)
对象中负责存放引用,以确保对象可以操作分配给该对象的变量以及调用类中的方法。
你可以把车钥匙和车看作是一组对象引用和对象的组合。
当你想要开车的时候,你首先需要拿出车钥匙点击开锁的选项,停车时,你需要点 击加锁来锁车。
车钥匙相当于就是引用,车就是对象,由车钥匙来驱动车的加锁和开锁。
并且,即使没 有车的存在,车钥匙也是一个独立存在的实体
也就是说,你有一个对象引用,但你不一定需要一个对象与之关联
也就是 Car carKey;
这里创建的只是引用,而并非对象
但是如果你想要使用这个引用时,会返回一个异常,告诉你需要 —个对象来和这个引用进行关联。
一种安全的做法是,在创建对象引用时同时把一个对象赋绐它。
Car carKey = new Car();
对象的引用和实体之间的关系
对象的实体
分配给对象的变量习惯地称做对象的实体
避免使用空对象
没有实体的对象称作空对象,
空对象不能使用,即不能让一个空对象去调用方法产生行为。
垃圾收集
一个类声明的两个对象如果具有相同的引用,那么二者就具有完全相同的实体
“垃圾收集”机制:周期地检测某个实体,是否已不再被任何对象所拥有(引用),如果发现,就释放实体占有的内存。
代码:
Vehicle carOne = new Vehicle ();
Vehicle carTwo = new Vehicle );
carOne.speedUp(60);
carTwo.speedUp(90);
Vehicle carOne = new Vehicle ();
Vehicle carTwo = new Vehicle );
carOne.speedUp(60);
carTwo.speedUp(90);
对象各自改变直接Speed
代码:
carOne = carTwo
carOne = carTwo
car1 和car 2具有同样的引用
属性和方法(一个类最基本的要素)
属性
属性也被称为字段,它是类的重要组成部分
属性可以是任意类型的对象,也可以是基本数据类型
示例
属性
方法
类中还应该包括方法,方法表示的是做某些事情的方式。
方法其实就是函数,只不过Java习惯把函数 称为方法。
这种叫法也体现了面向对象的概念。
一般格式为
一般格式为
方法声明部分 {
方法体的内容
}
方法体的内容
}
方法的定义包括两部分、方法的基本组成
方法声明
方法声明,包括方法名和方法的返回类型,方法的参数
double getSpeed() {
return speed;
}
return speed;
}
方法名称
get Result就是方法名称
方法的名字
方法的参数
()里面表示方法接收的参数
有一种特殊的参数类型,void,表示方法无返 回值。
方法的参数
方法的返回类型,返回值
方法的返回类型
return表示方法的返回值
方法的返回值必须和方法的参数类型保持一致
方法体
{}包含的代码段被称为方法体。
方法声明之后的一对大括号“{” ,“}”以及之间的内容称作方法的方法体。方法体的内容包括局部变量的声明和Java语句。
类方法的定义
实例方法
方法声明时,方法类型前面不加关键字static修饰的是实例方法
对象调用实例方法
当对象调用实例方法时,该方法中出现的实例变量就是分配给该对象的实例变量;
该方法中出现的类变量也是分配给该对象的变量,只不过这个变量和所有的其他对象共享而已。
类方法
方法声明时,方法类型前面加static关键字修饰的是类方法(静态方法)
类名调用类方法
类方法不仅可以被类创建的任何对象调用执行,也可以直接通过类名调用。
和实例方法不同的是,类方法不可以操作实例变量,这是因为在类创建对象之前,实例成员变量还没有分配内存。
案例
class A {
int a;
float max(float x,float y) { //实例方法
…
}
static float jerry() { //类方法
…
}
static void speak(String s) { //类方法
…
}
}
int a;
float max(float x,float y) { //实例方法
…
}
static float jerry() { //类方法
…
}
static void speak(String s) { //类方法
…
}
}
Java应用程序的基本结构
一个Java应用程序是由若干个类所构成,但必须有一个主类,即含有main方法的类,Java应用程序总是从主类的main方法开始执行。
编写一个Java应用程序时,可以编写若干个Java源文件,每个源文件编译后产生一个类的字节码文件。
例子2中的三个Java源文件MainClass.java 、Circle.java 、Lader.java都保存在c:\ch2中。
其中MainClass.java是含有主类的Java源文件。
编译: C:\ch2 >javac MainClass.java
运行: C:\ch2 >java MainClass
编译: C:\ch2 >javac MainClass.java
运行: C:\ch2 >java MainClass
Circle.java
public class Circle
{
double radius;
double getArea()
{
return 3.1415926*radius;
}
}
{
double radius;
double getArea()
{
return 3.1415926*radius;
}
}
Lader .java
public class Lader
{
double above; //梯形的上底
double bottom;
double height;
double getArea()
{
return (above+bottom)*height/2;
}
}
{
double above; //梯形的上底
double bottom;
double height;
double getArea()
{
return (above+bottom)*height/2;
}
}
MainClass .java
public class MainClass
{
public static void main(String args[])
{
Circle circle=new Circle();
circle.radius=100;
double area=circle.getArea();
System.out.println("圆的面积:"+area);
Lader lader=new Lader();
lader.above=10;
lader.bottom=56;
lader.height=8.9;
area=lader.getArea();
System.out.println("梯形的面积:"+area);
}
}
{
public static void main(String args[])
{
Circle circle=new Circle();
circle.radius=100;
double area=circle.getArea();
System.out.println("圆的面积:"+area);
Lader lader=new Lader();
lader.above=10;
lader.bottom=56;
lader.height=8.9;
area=lader.getArea();
System.out.println("梯形的面积:"+area);
}
}
在一个源文件中编写多个类
Java允许在一个Java源文件中编写多个类,但其中的多个类至多只能有一个类使用public修饰。
例2-3中的Java源文件Rectangle.java包含有两个类
重要步骤:
1. 命名保存源文件
源文件命名保存为Rectangle.java(回忆一下源文件命名的规定)
2. 编译: C:\ch2\>javac Rectangle.java
编译成功,ch2目录下就会有两个字节码文件.
3. 执行: C:\chapter1\>java Example2_3
java 命令后的名字必须是主类的名字
重要步骤:
1. 命名保存源文件
源文件命名保存为Rectangle.java(回忆一下源文件命名的规定)
2. 编译: C:\ch2\>javac Rectangle.java
编译成功,ch2目录下就会有两个字节码文件.
3. 执行: C:\chapter1\>java Example2_3
java 命令后的名字必须是主类的名字
构造方法/构造函数/构造函数重载
是什么?
构造方法比较特殊
构造方法是一种特殊方法
构造方法是类的一种特殊方法
在Java中,通过 提供这个构造器,来确保每个对象都被初始化。
在Java中,有一种特殊方法被称为 构造方法,也被称为构造函数、构造器等。
作用
主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值
它承担着初始化对象数据成员的任务。
总与new 运算符一起使用在创建对象的语句中。
只能与 new 运算符结合使用
用来初始化类的一个新的对象,在创建对象(new 运算符)之后自动调用。
每当新对象被创建的时候,构造函数就会被调用
当新对象被创建时,构造函数会被调用。
特殊之处/特点
每个类都有构造函数(默认构造函数)
每一个类都有构造函数。
构造函数与默认构造函数
每个类都有一个默认的构造方法
方法名称必要要与类名保持一致
方法名必须与类名相同
它的名字必须与它所在的类的名字完全相同
构造器的函数名称必须和它所属的类的名称相同。
没有参数类型和返回值
可以有 0 个、1 个或多个参数
没有任何返回值,包括 void
默认返回类型就是对象类型本身
它没有参数类型和返回值
没有类型
构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型
不会被继承,且可以有范围修饰符。
不能被 static、final、synchronized、abstract 和 native(类似于 abstract)修饰
构造函数的重载
Java中构造函数重载和方法重载很相似。
可根据其参数个数的不同或参数类型的不同来区分它们,即构造函数的重载。
可以为一个类创建多个构造函数。
每个类可以有一个以上的构造方法
构造方法可以有多个
每一个构造函数必须有它自己唯一的参数列表。
一个类可以有多个构造函数,但必须保证他们的参数不同,即参数的个数不同,或者是参数的类型不同。
允许一个类中编写若干个构造方法
构造方法与对象的创建
构造方法和对象的创建密切相关 。
构造方法,只能在对象的创建时期调用一次,保证了对象初始化的进行。
默认的构造方法
每个类都有一个默认的构造方法,并且可以有一个以上的构造方法。
默认的构造方法也被称为默认构造器或者无参构造器。
JVM会默认添加一个无参的构造器
在程序员没有给类提供构造函数的情况下,Java编译器会为这个类创建一个默认的构造函数。
如果在编写一个可实例化的类时没有专门编写构造函数,多数编程语言会自动生成缺省构造器(默认构造函数)。
如果类中没有编写构造方法,系统会默认该类只有一个构造方法,该默认的构造方法是无参数的,且方法体中没有语句
其中,不加任何参数的构造方法被称为默认的构造方法
也就是Apple applel = new Apple()
如果类中没有定义任何构造方法,那么JVM会为你自动生成一个构造方法
构造方法的示例
上面代码(d不会发生编译错误,因为Apple对象包含了一个默认的构造方法。
如果类里定义了一个或多个构造方法,那么Java不提供默认的构造方法 。
但是如果手动定义了任何一个构造方法,JVM就不再为你提供默认的构造器,
你必须手动指定,否则会出现编译错误。
编译错误
显示的错误是,必须提供Apple带有int参数的构造函数,而默认的无参构造函数没有被允许使用。
默认构造函数一般会把成员变量的值初始化为默认值,如int ->0,Integer -> null。
Java不支持复制构造函数
Java不支持像C++中那样的复制构造函数
这个不同点是因为,如果你不自己写构造函数的情况下,Java不会创建默认的复制构造函数。
构造方法的示例
多态的体现
构造方法的示例
上面定义了一个Apple类,
你会发现这个Apple类没有参数类型和返回值,并且有多个以Apple同名 的方法,而且各个Apple的参数列表都不一样
这其实是一种多态的体现
四种不同的构造方法
在定义完成 构造方法后,就能够创建Apple对象了。
构造方法的示例
如上面所示,定义了四个Apple对象,并调用了 Apple的四种不同的构造方法
常见问题
Java中,什么是构造函数?什么是构造函数重载?什么是复制构造函数?
transient关键字
是什么?
Java 语言的关键字
变量修饰符,
防止对象进行序列化操作
声明为static和transient类型的变量不能被序列化
如果用transient 声明一个实例变量,当对象存储时,它的值不需要维持。
这里的对象存储是指,Java 的serialization 提供的一种持久化对象实例的机制。
当一个对象被序列化时,transient 型变量的值不包括在序列化的表示中,然而非transient 型的变量是被包括进去的。
使用情况
当持久化对象时,可能有一个特殊的对象数据成员,不想用serialization 机制来保存它。
为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。
源码使用场景
在关于java 的集合类的学习中,发现ArrayList 类和Vector 类都是使用数组实现的,
但是在定义数组elementData 这个属性时稍有不同,那就是ArrayList 使用transient 关键字
ArrayList 使用transient 关键字
简单点说
被transient 修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值,被设为初始值,
如int 型的是0,对象型的是null。
static关键字
是什么?
static是我们日常生活中经常用到的关键字, 也是Java中非常重要的一个关键字
static是Java中的关键字,表示静态的
static关键字表示的概念是全局的、静态的
static用在没有创建对象的情况下,调用方法/变量。
表明一个成员变量或者是成员方法可以在没有所属类的实例变量(没有new)的情况下被访问。
表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
static修饰
变量/成员变量
静态变量
static可以修饰成员变量
用它修饰的变量被称为静态变量。
用static声明的成员变量为静态成员变量,也称为类变量
静态变量常用static关键来修饰,表示通用资源或可以被所有的对象所使用。
样例代码
static修饰变量
static String name = "cxuan";
静态变量是属于这个类所有的
static关键字只能定义在类的{}中,而不能定义在任何方法中。
用static 表示变量的级别,一个类中的静态变量,不属于类的对象或者实例。
static属于类所有
静态变量是属于这个类所有的
就算把方法中的static关键字去掉也是一样的。
如果静态变量未被私有化,可以用“类名.变量名”的方式来使用。
因为静态变量与所有的对象实例共享,因此他们不具线程安全性。
由类来直接调用static修饰的变量, 它不需要手动实例化类进行调用
它不需要手动实例化类进行调用
类变量的生命周期和类相同,在整个应用程序执行期间都有效。
方法/成员方法
使用static修饰的方法称为静态方法
static可以修饰方法
被static修饰的方法被称为静态方法
其实,就是在一个方法定义中加上static关键字进行修饰
public class StaicBlock {
static{
System.out.println("I'm A static code block");
}
}
static{
System.out.println("I'm A static code block");
}
}
在一个方法定义中加上static关键字进行修饰
用来修饰成员变量和成员方法,也可以形成静态static代码块。
《Java编程思想》中关于static关键字修饰方法的定义
static方法就是没有this的方法, 在static内部不能调用非静态方法,
反过来是可以的, 在static外部能调用非静态方法,
而且可以在没有创建任何对象的前提下, 仅仅通过类本身来调用static方法,
这实际上是static方法的主要用途
平常见的最多的static方法就是main方法
至于为什么main方法必须是static的, 现在应该很清楚了
因为程序在执行main方法的时候没有创建任何对象, 因此只有通过类名来访问,
main()方法就是Java 程序入口点,是静态方法。
static修饰方法的注意事项
不用创建对象,直接类名,变量名即可访问
最常用的
与静态变量一样,静态方法是属于类,而不是实例。
可以在不用创建对象的前提下,就能够访问static方法
static方法就是没有this的方法, 在static内部不能调用非静态方法,
由于静态方法不依赖于任何对象就可以直接访问,因此对于静态方法来说,是没有this关键字的,实例变量都会有this关键字。
静态方法能够直接使用类名.方法名进行调用。
可以在不用创建对象的前提下,就能够访问static方法
如何做到呢?
在上面的例子中, 由于static Method是静态方法, 所以能够使用类名.变量名进行调用。
通常静态方法,通常用于想给其他的类使用而不需要创建实例。
例如:Collections class(类集合)。
Java 的包装类和实用类包含许多静态方法
Java 的包装类和实用类包含许多静态方法
static修饰的方法内部,不能调用非静态方法
在静态方法中不能访问类的非静态成员变量和非静态方法,
一个静态方法只能使用静态变量和调用静态方法
图解
static修饰的方法内部,不能调用非静态方法
非静态方法内部可以调用static静态方法
非静态方法内部可以调用static静态方法
只需要进行一次的初始化操作放在static代码块中
由于静态代码块随着类的加载而执行
因此,很多时候会将只需要进行一次的初始化操作放在static代码块中进行。
从Java8 以上版本开始也可以有接口类型的静态方法了。
代码块
代码块分为两种
使用{}代码块
static{}静态代码块
static关键字可以用来修饰代码块,
static修饰的代码块被称为静态代码块。
静态代码块可以置于类中的任何地方
类中可以有多个static块
在类初次被加载时, 会按照static代码块的顺序来执行
每个static修饰的代码块只能执行一次。
Java 的静态块是一组指令在类装载的时候在内存中由Java ClassLoader 执行。
静态块常用于初始化类的静态变量。
大多时候还用于在类装载时候创建静态资源。
Java 不允许在静态块中使用非静态变量。
一个类中可以有多个静态块,尽管这似乎没有什么用。
代码块的加载顺序
静态代码块的例子
静态代码块的例子
static代码块可以用来优化程序执行顺序, 是因为它的特性:
只会在类加载时执行一次。
静态块只在类装载入内存时,执行一次。
static代码块可以用来优化程序执行顺序
静态类
Java 可以嵌套使用静态类,但是静态类不能用于嵌套的顶层。
静态嵌套类的使用与其他顶层类一样,嵌套只是为了便于项目打包。
做静态代码块/形成静态static代码块
static用作静态内部类
内部类的使用场景比较少,但是内部类还有具有一些比较有用的
内部类的分类
静态内部类
普通内部类
局部内部类
匿名内部类
静态内部类是什么?
用static修饰的内部类
可以包含静态成员, 也可以包含非静态成员
在非静态内部类中,不可以声明静态成员
静态内部类的作用
由于非静态内部类的实例创建,需要有外部类对象的引用
非静态内部类对象的创建必须依托于外部类的实例
而静态内部类的实例创建,只需依托外部类
由于非静态内部类对象持有了外部类对象的引用
因此非静态内部类可以访问外部类的非静态成员:
而静态内部类只能访问外部类的静态成员
内部类需要脱离外部类对象来创建实例
避免内部类使用过程中出现内存溢出
避免内部类使用过程中出现内存溢出
静态导包
导包方式1:直接import导入包
使用了java.util内的工具类时, 你需要导入java.util包,才能使用其内部的工具类
举例
使用了java.util内的工具类时, 你需要导入java.util包,才能使用其内部的工具类
导包方式2:使用静态导包import static
什么是静态导包
使用import static用来导入某个类或者某个包中的静态方法或者静态变量。
举例
使用静态导包import static
类加载顺序/类的初始化顺序/Java执行顺序
static修饰的变量和静态代码块在使用前已经被初始化好了,类的初始化顺序依次是
解答1
加载父类的静态字段→父类的静态代码块
->子类静态字段→子类静态代码块
→父类成员变量(非静态字段)→父类非静态代码块→父类构造器
→子类成员变量(非静态字段)→子类非静态代码块→子类构造器
解答2
父类静态代码块
→子类静态代码块
→父类非静态代码块→父类构造函数
→子类非静态代码块→子类构造函数
static进阶知识
static所修饰的属性和方法都属于类的
static所修饰的属性和方法都属于类的, 不会属于任何对象;
它们的调用方式都是类名, 属性名/方法名,而实例变量和局部变量都是属于具体的对象实例,
static修饰变量的存储位置:方法区
JVM的不同存储区域
JVM的不同存储区域
方法区
各个线程共享的内存区域
用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
static修饰的变量存储在方法区中
堆
堆是线程共享的数据区
堆是JVM中最大的一块存储区域
所有的对象实例, 包括实例变量都在堆上进行相应的分配。
虚拟机栈
线程私有的数据区
Java虚拟机栈的生命周期与线程相同
虚拟机栈也是局部变量的存储位置。
方法在执行过程中, 会在虚拟机栈种创建一个栈帧(stack frame) 。
程序计数器
线程私有的数据区
这部分区域用于存储线程的指令地址
用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成。
本地方法栈
线程私有的数据区
本地方法栈存储的区域主要是Java中使用native关键字修饰的方法所存储的区域
static变量的生命周期与类的生命周期相同
static变量的生命周期与类的生命周期相同
随类的加载而创建, 随类的销毁而销毁;
普通成员变量和其所属的生命周期相同。
声明为static和transient类型的变量不能被序列化
序列化目的
为了把Java对象转换为字节序列
对象转换为有序字节流, 以便其能够在网络上传输或者保存在本地文件中,
声明为static和transient类型的变量不能被序列化
static关键字
因为static修饰的变量保存在方法区中, 只有堆内存才会被序列化。
transient关键字
防止对象进行序列化操作
是什么?
Java 语言的关键字
变量修饰符,
如果用transient 声明一个实例变量,当对象存储时,它的值不需要维持。
这里的对象存储是指,Java 的serialization 提供的一种持久化对象实例的机制。
当一个对象被序列化时,transient 型变量的值不包括在序列化的表示中,然而非transient 型的变量是被包括进去的。
transient 关键字的作用是控制变量的序列化
在变量声明前加上该关键字,可以阻止该变量被序列化到文件中
被transient 修饰的成员变量,在序列化的时候其值会被忽略
在被反序列化后,transient 变量的值,被设为初始值
int 型的是0
对象型的是null
使用情况
当持久化对象时,可能有一个特殊的对象数据成员,不想用serialization 机制来保存它。
为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。
用作日志打印,减少占用内存资源
在开发过程中, 经常会使用static关键字作为日志打印
这行代码你应该经常看到,用于打印日志的
但是这种打印日志的方式存在问题
对于每个Static Test的实例化对象都会拥有一个LOGGER
如果创建了1000个Static Test对象, 则会多出1000个Logger对象, 造成资源的浪费
因此,通常会将Logger对象声明为static变量, 这样一来,能够减少对内存资源的占用。
static经常用作单例模式
由于单例模式指的就是对于不同的类来说, 它的副本只有一个, 因此static可以和单例模式完全匹配。
经典的双重校验锁实现单例模式的场景
双重校验锁实现单例模式
使用static,保证singleton变量是静态的
使用volatile,保证singleton变量的可见性
使用私有构造器,确保Singleton不能被new实例化,
使用Singleton.getInstance(), 获取singleton对象
首先会进行判断, 如果singleton为空, 会锁住Singletion类对象
为什么需要两次判断 ?
如果线程t1执行到singleton==null后, 判断对象为null, 此时线程把执行权交给了t2, t2判断对象
为null, 锁住Singleton类对象, 进行下面的判断和实例化过程。
为null, 锁住Singleton类对象, 进行下面的判断和实例化过程。
如果不进行第二次判断的话, 那么t1在进行第一次判空后,也会进行实例化过程,此时仍然会创建多个对象。
类的构造器是否是static的
类的构造器,虽然没有用static修饰, 但是实际上是static方法
static最简单、最方便记忆的规则就是,没有this引用。
角度1:类的构造器
而在类的构造器中, 是有隐含的this绑定的,因为构造方法是和类绑定的,从这个角度来看,构造器不是静态的。
角度2:类的方法
从类的方法这个角度来看,因为类.方法名不需要新创建对象就能够访问,所以从这个角度来看,构造器也不是静态的
角度3:JVM指令角度
从JVM指令角度去看, 来看一个例子
从JVM指令角度去看
使用java p-c生成StaticTest的字节码看一下
生成StaticTest的字节码
而且在JVM规范中说到
JVM规范,invokespecial指令
https://docs.oracle.com/javase/specs/jvms/se7/htm/jvms-6.html#jvms-6.5.invoke static
在调用static方法时是使用的invoke static指令, new对象调用的是invokespecial指令,
从这个角度来讲, invoke static指令是专门用来执行static方法的指令;
invoke special是专门用来执行实例方法的指令;从这个角度来讲,构造器也不是静态的。
原文地址
https://zhuanlan.zhihu.com/p/26819685
static常见问题
”static”关键字是什么意思?
是否可以重写/覆盖(override)一个private的方法?
不可以,private只能被自身类访问;
是否可以重写/覆盖(override)一个static的方法?
Java中static方法不能被覆盖
static方法跟类的任何实例都不相关,所以概念上不适用。
因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。
不可以,static是编译时静态绑定的,override是运行时动态绑定的
是否可以在static环境中访问非static变量?
不可以,static是静态常量,属于类,调用非static变量需要new,而类的加载先于实例创建。
static变量在Java中是属于类的,它在所有的实例中的值是一样的。
当类被Java虚拟机载入的时候,会对static变量进行初始化。
如果代码尝试不用实例,来访问非static的变量,编译器会报错
因为这些变量还没有被创建出来,还没有跟任何实例关联上。
final关键字
是什么?
Java 中的一个关键字
表示的是“这部分是无法修改的”。
final的意思是最后的、最终的。
final关键字可以修饰
变量
方法中的局部变量
成员变量
类
final类
final修饰类时,表明这个类不能被继承。
final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
可以使用final将类声明为final类。final类不能被继承,即不能有子类。
如果把任何一个类声明为final,则不能继承它。
如:
final class A {
…
}
final class A {
…
}
final class Parent {
}
}
以上类不能被继承!
方法
final方法
final修饰方法时,表明这个方法不能被任何子类重写,
如果只有在想明确禁止该方法在子 类中被覆盖的情况下才将方法设置为final。
如果用final修饰父类中的一个方法,那么这个方法不允许子类重写。
如果任何方法声明为final,则不能覆盖它。
class Parent{
final void name() {
System.out.println("Hollis");
}}
final void name() {
System.out.println("Hollis");
}}
当定义以上类的子类时,无法覆盖其name 方法,会编译失败。
final修饰变量分为两种情况
一种是修饰基本数据类型,表示数据类型的值不能被修改;
一种是 修饰引用类型,表示对其初始化之后便不能再让其指向另一个对象。
用法跟final 相似的关键字const
const 是Java 预留关键字,
用于后期扩展用,用法跟final 相似,不常用。
方法覆盖和方法重载
方法重载/Overload
定义
方法重载(Overloading)
它是类名的不同表现形式
在Java中一个很重要的概念是方法的重载
重载Overload是一个类中多态性的一种表现
重载只是一种语言特性,是一种语法规则,与多态无关,与面向对象也无关。
函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
描述的是同一类中的
方法能够在同一个类中或者在一个子类中被重载
发生在同一个类里面,两个或者是多个方法的方法名相同,但是参数不同的情况。
在一个类中,同名的方法,如果有不同的参数列表,则视为重载。
一个类中可以有多个方法具有相同的名字,但这些方法的参数必须不同,即或者是参数的个数不同,或者是参数的类型不同。
允许一个类中存在多个方法名相同的方法,只要他们参数列表不同即可;
方法的返回类型可以相同也可以不相同
与返回值类型无关
被重载的方法可以改变返回类型
方法的返回类型可以相同也可以不相同
对返回类型没有要求,可以相同也可以不同
返回值类型,可以相同也可以不相同。
仅仅返回类型不同,不足以成为方法的重载
无法以返回类型作为重载函数的区分标准
不能通过返回类型是否相同,来判断重载
方法名相同,参数列表不相同
方法名称必须相同
参数列表必须不同
同名方法的参数列表不同
被重载的方法必须改变参数列表
JVM通过参数列表,调用不同的方法
参数列表不同指的是
参数的顺序不同
参数类型排列顺序不同
参数个数不同
参数类型不同
注意: int fun(int x)和int fun(int y)不算重载
其他重载的条件
被重载的方法可以改变访问修饰符
被重载的方法可以声明新的或更广的检查异常
重载是一个编译期概念
重载遵循所谓“编译期绑定”
重载是发生在编译时的,因为编译器可以根据参数的类型来选择使用哪个方法
即在编译时,根据参数变量的类型判断应该调用哪个方法。
因为在编译期已经确定调用哪个方法,所以重载并不是多态。
(注:严格来说,重载是编译时多态,即静态多态)
但是,Java 中提到的多态,在不特别说明的情况下,都指动态多态
构造函数也是重载的一种,另外一种就是方法的重载
方法重载
一种是Apple构造函数的重载
一种是getApple方法的重载。
要是有几个相同的名字,Java如何知道你调用的是哪个方法呢?
每个重载的方法都有独一无二的参数列表。
其中包括参数的类型、顺序、参数数量等,满 足一种一个因素就构成了重载的必要条件。
重载的例子1
重载的例子1
上面的代码中,定义了两个bark 方法,
一个是没有参数的bark 方法,
另外一个是包含一个int 类型参数的bark 方法。
在编译期,编译期可以根据方法签名(方法名和参数情况)情况确定哪个方法被调用。
重载的例子2
Father类中sayHello方法是重载方法
public class Father {
public static void main(String[] args) {
Father s = new Father();
s.sayHello();
s.sayHello("wintershii");
}
public void sayHello() {
System.out.println("Hello");
}
public void sayHello(String name) {
System.out.println("Hello" + " " + name);
}
}
public static void main(String[] args) {
Father s = new Father();
s.sayHello();
s.sayHello("wintershii");
}
public void sayHello() {
System.out.println("Hello");
}
public void sayHello(String name) {
System.out.println("Hello" + " " + name);
}
}
重载的例子3
A类中add方法是重载方法
class A {
float add(int a,int b) {
return a+b;
}
float add(long a,int b) {
return a+b;
}
double add(double a,int b) {
return a+b;
}
}
float add(int a,int b) {
return a+b;
}
float add(long a,int b) {
return a+b;
}
double add(double a,int b) {
return a+b;
}
}
方法覆盖/方法重写/Override
定义
又叫方法重写(Override)
重新写一遍
就是子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,
所以在方法名,参数列表,返回类型都相同的情况下, 对方法体进行修改或重写
返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)
方法重写的目的
子类通过重写可以隐藏已继承的实例方法。
子类通过方法的重写可以隐藏继承的方法,子类通过方法的重写可以把父类的状态和行为改变为自身的状态和行为。
描述是对子类和父类之间的
发生在父类与子类之间
Java的子类与父类中有两个名称、参数列表都相同的方法
子类中定义一个方法,这个方法的类型和父类的方法的类型一致或者是父类的方法的类型的子类型,
并且这个方法的名字、参数个数、参数的类型和父类的方法完全相同。
子类中的新方法将覆盖父类中原有的方法
由于他们具有相同的方法签名,所以子类中的新方法将覆盖父类中原有的方法。
子类重新定义了父类的方法。
在子类中,把父类本身有的方法,重新写一遍。
方法名,参数列表,返回类型必须相同
方法覆盖必须有相同的方法名,参数列表和返回类型。
(除过子类中方法的返回类型是父类中返回类型的子类)
方法的名字、参数个数、参数的类型和父类的方法完全相同。
方法的类型和父类的方法的类型一致或者是父类的方法的类型的子类型
子类重新定义父类的方法,重写必须有相同方法名,参数列表和返回类型
子类函数的访问修饰权限不能少于父类的
访问修饰符的限制一定要大于被重写方法的访问修饰符
public>protected>default>private
覆盖者可能不会限制它所覆盖的方法的访问。
重写方法一定不能抛出新的检查异常,或者比被重写方法申明更加宽泛的检查型异常
重写是一个运行期间概念
重写遵循所谓“运行期绑定”
即在运行时,根据引用变量所指向的实际对象的类型来调用方法。
而重写是多态。
方法重写的原则以及语法规则
重写的方法可以使用@Override注解来标识
重写的方法必须要和父类保持一致,包括返回值类型,方法名,参数列表也都一样
子类中重写方法的访问权限,不能低于父类中方法的访问权限。
重写父类的方法时,不可以降低方法的访问权限。
如:
class A {
protected float f(float x,float y) {
return x-y;
}
}
class B extends A {
float f(float x,float y) {//非法,因为降低了访问权限
return x+y ;
}
}
class C extends A {
public float f(float x,float y) {//合法,提高了访问权限
return x*y ;
}
}
class A {
protected float f(float x,float y) {
return x-y;
}
}
class B extends A {
float f(float x,float y) {//非法,因为降低了访问权限
return x+y ;
}
}
class C extends A {
public float f(float x,float y) {//合法,提高了访问权限
return x*y ;
}
}
如果子类可以继承父类的某个实例方法,那么子类就有权利重写这个方法。
重写方法一定不能抛出新的检查异常或比被重写的方法声明的检查异常更广泛的检查异常。
重写的方法能够抛出更少或更有限的异常(也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明)。
不能重写被标识为final 的方法。
如果不能继承一个方法,则不能重写这个方法。
JDK 1.5对方法重写的改进
在JDK 1.5版本之后,允许重写方法的类型可以是父类方法的类型的子类型,即不必完全一致.
也就是说,如果父类的方法的类型是“类”,重写方法的类型可以是“子类”。
重写的条件
参数列表必须完全与被重写方法的相同;
返回类型必须完全与被重写方法的返回类型相同;
访问级别的限制性一定不能比被重写方法的强;
访问级别的限制性可以比被重写方法的弱;
重写的例子1
重写的例子
子主题
输出结果:
bowl
上面的例子中,dog 对象被定义为Dog 类型。
在编译期,编译器会检查Dog 类中是否有可访问的bark()方法,只要其中包含bark()方法,那么就可以编译通过。
在运行期,Hound 对象被new 出来,并赋值给dog 变量,这时,JVM 是明确的知道dog 变量指向的其实是Hound 对象的引用。
所以,当dog 调用bark()方法的时候,就会调用Hound类中定义的bark()方法。这就是所谓的动态多态性。
重写的例子2
例如
子类Apple中的方法和父类Fruit中的方法同名,
上面这段代码描述的就是重写的代码
重写的例子3
public class Father {
public static void main(String[] args) {
Son s = new Son();
s.sayHello();
}
public void sayHello() {
System.out.println("Hello");
}
}
class Son extends Father{
@Override
public void sayHello() {
System.out.println("hello by ");
}
}
public static void main(String[] args) {
Son s = new Son();
s.sayHello();
}
public void sayHello() {
System.out.println("Hello");
}
}
class Son extends Father{
@Override
public void sayHello() {
System.out.println("hello by ");
}
}
初始化
类的初始化
上面创建出来了一个Car这个对象,其实在使用new关键字创建一个对象时,其实是调用了 这个对象无参数的构造方法进行的初始化
这段代码
这个无参数的构造函数可以隐藏,由JVM自动添加
即,构造函数能够确保类的初始化。
成员初始化
Java会尽量保证每个变量在使用前都会获得初始化
涉及两种方式
编译器默认指定的字段初始化
基本数据类型的初始化
子主题
—种是其他对象类型的初始化
String也是一种对象,对象的初始值都为null,
其中也包括基本类型的包装类。
指定数值的初始化
int a = 11
指定a的初始化值不是0,而是11。
其他基本类型和对象类型也是一样的。
构造器初始化
可以利用构造器来对某些方法和某些动作进行初始化,确定初始值
例如
利用构造函数,能够把i的值初始化为11
初始化顺序
有哪些需要探讨的初始化顺序
静态属性
static开头定义的属性
静态方法块
static {}包起来的代码块
普通属性
非static定义的属性
普通方法块
{}包起来的代码块
构造函数
类名相同的方法
方法
普通方法
举例有哪些需要探讨的初始化顺序
举例
这段代码的执行结果就反应了它的初始化顺序
静态属性初始化
静态方法块初始化
普通属性初始化
普通方法块初始化
构造函数初始化
数组初始化
数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。
数组是通过方 括号下标操作符 口 来定义使用。
—般数组是这么定义的
—般数组的定义
两种格式的含义是一样的。
直接绐每个元素赋值:int array[4] = {1,2,3,4};
给一部分赋值,后面的都为0 : int array[4] = {1,2};
由赋值参数个数决定数组的个数:int array[] = {1,2};
可变参数列表
数组冷门的用法
可变参数的定义
可变参数的定义
可变参数的调用
可变参数的调用
销毁
不需要手动管理对象的销毁工 作
虽然Java语言是基于C++的,但是它和C/C++ —个重要的特征就是不需要手动管理对象的销毁工 作。
在著名的一书《深入理解Java虚拟机》中提到一个观点
Java与C++之间有一堵“高墙”
由内存动态分配和垃圾收集技术所围成的
墙外面的人 想进去,墙里面的人却想出来。
在Java中,不再需要手动管理对象的销毁,它是由Java虚拟机进行管理和销毁的
虽然不需要手动管理对象,但是需要知道对象作用域这个概念。
对象作用域
多数语言都有作用域(scope)这个概念。
作用域,决定了其内部定义的变量名的可见性和生命周期。
在 C、C++和Java中,作用域通常由{}的位置来决定
例如
a变量会在两个{}作用域内有效,而b变量的值只能在它自己的{}内有效。
虽然存在作用域,但是不允许这样写
Java中不允许的写法
这种写法在C/C++中是可以的,但是在Java中不允许这样写
因为Java设计者认为这样写会导致程序混乱
this 和 super
this和super都是Java中的关键字
this
this
表示某个对象
表示的当前对象
this可调用方法、调用属性和指向对象本身
什么时候可以省略this ?
当一个对象调用方法时,方法中的实例成员变量就是指分配给该对象的实例成员变量,而static变量则和其他对象共享。
因此,通常情况下,可以省略实例成员变量名字前面的“this.”,以及static变量前面的“类名.”。
但是,当实例成员变量的名字和局部变量的名字相同时,成员变量前面的“this.”或“类名.”就不可以省略。
其他场景:区分成员变量和局部变量
如果局部变量的名字与成员变量的名字相同,则成员变量被隐藏,即这个成员变量在这个方法内暂时失效
如果想在该方法中使用被隐藏的成员变量,必须使用关键字this
为什么this不能出现在类方法中?
this可以出现在实例方法和构造方法中,但不可以出现在类方法中。
实例方法可以操作类的成员变量,当实例成员变量在实例方法中出现时,默认的格式是:this.成员变量;
当static成员变量在实例方法中出现时,默认的格式是:类名.成员变量;
因为类方法可以通过类名直接调用
这时,可能还没有任何对象诞生。
this在Java中的使用一般有三种
和构造函数一起使用,充当一个全局关键字的效果
和构造函数一起使用,充当一个全局关键字的效果
你会发现上面这段代码使用的不是this,而是this(参数)
它相当于调用了其他构造方法,然后传递 参数进去。
注意:this必须放在构造方法的第一行,否则编译不通过
this必须放在构造方法的第一行,否则编译不通过
修饰属性
最常见的就是在构造方法中使用this
在构造方法中使用this
在构造方法中使用this
main方法中传递了一个int值为10的参数,
它表示的就是苹果的数量,并把这个数量赋绐了 num全 局变量。
所以num的值现在就是10。
指向当前对象本身
指向当前对象本身
一个eatAppleO方法可以调用多次,你在后面还可以继续调 用。其实就是this,
在eat Apple方法中加了一个return this的返回值,
哪个对象调用eatApple方法,都能返回对象的自身。
super
如果把this理解为指向自身的一个引用,那么super就是指向父类的一个引用。super关键字和this —样
使用super.对象来引用父类的成员
使用super.对象 来引用父类的成员
使用super操作被隐藏的成员变量和方法
在子类中想使用被子类隐藏的成员变量或方法就可以使用关键字super。
比如super.x、super.play()就是访问和调用被子类隐藏的成员变量x和方法play()。
使用super调用父类的构造方法
使用super参数来调用父类的构造函数
用子类的构造方法创建一个子类的对象时,子类的构造方法总是先调用父类的某个构造方法,
也就是说,如果子类的构造方法没有明显地指明使用父类的哪个构造方法,子类就调用父类的不带参数的构造方法,
即如果在子类的构造方法中,没有明显地写出super关键字来调用父类的某个构造方法,那么默认地有:super();
子类不继承父类的构造方法,因此,子类在其构造方法中需使用super来调用父类的构造方法,而且super必须是子类构造方法中的头一条语句。
this关键字和super关键字的比较
this关键字和super关键字的比较
编程风格
Allmans风格
也称“独行”风格,
即左、右大括号各自独占一行。
Kernighan风格
也称“行尾”风格,
即左大括号在上一行的行尾,而右大括号独占一行 。
注释
目的:有利于代码的维护和阅读
Java支持两种格式的注释
1)单行注释
使用“//”表示单行注释的开始,即该行中从“//”开始的后续内容为注释.
2) 多行注释
使用“/*”表示注释的开始,以“*/”表示注释结束.
参数传值和传值机制、可变参数
方法中最重要的部分之一就是方法的参数,参数属于局部变量,
当对象调用方法时,参数被分配内存空间,并要求调用者向参数专递值,即方法被调用时,参数变量必须有具体的值。
基本数据类型参数的传值
对于基本数据类型的参数,向该参数传递的值的级别不可以高于该参数的级别 。
引用类型参数的传值
当参数是引用类型时,“传值”传递的是变量中存放的“引用”,而不是变量所引用的实体。
引用类型参数的传值
可变参数
可变参数是指在声明方法时不给出参数列表中从某项直至最后一项参数的名字和个数,但这些参数的类型必须相同。
可变参数使用“…”表示若干个参数,这些参数的类型必须相同,最后一个参数必须是参数列表中的最后一个参数。
例如: public void f(int … x)
方法f的参数列表中,从第1个至最后一个参数都是int型,但连续出现的int型参数的个数不确定。
称x是方法f的参数列表中的可变参数的“参数代表”。
参数代表可以通过下标运算来表示参数列表中的具体参数,即x[0],x[1]…x[m]分别表示x代表的第1个至第m个参数。
实参与形参
在Java 中定义方法的时候是可以定义参数的。
比如Java 中的main 方法,public static void main(String[] args),
这里面的args 就是参数。
参数在程序语言中分为
形式参数
是在定义函数名和函数体的时候使用的参数,
目的是用来接收调用该函数时传入的参数。
实际参数
在调用有参函数时,主调函数和被调函数之间有数据传递关系。
在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。
简单举个例子:
实际参数是调用有参方法的时候真正传递的内容
而形式参数是用于接收实参内容的参数。
值传递与引用传递
当我们调用一个有参函数的时候,会把实际参数传递给形式参数。
但是,在程序语言中,这个传递过程中传递的两种情况
值传递
引用传递
如何定义和区分值传递和引用传递?
值传递
对基本类型变量而言的
传递的是该变量的一个副本,改变副本不影响原变量。
是指在调用函数时将实际参数复制一份传递到函数中
这样在函数中如果对参数进行修改,将不会影响到实际参数
(pass by value)
引用传递
对于对象型变量而言
传递的是该对象地址的一个副本,并不是原对象本身。
是指在调用函数时将实际参数的地址直接传递到函数中
那么在函数中对参数所进行的修改,将影响到实际参数
(pass by reference)
值传递和引用传递之前的区别的重点
值传递和引用传递之前的区别的重点
举一个形象的例子(给钥匙)
你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了
这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,
那么这把钥匙还给你时,你自己的钥匙上也会多出他刻的名字。
那么这把钥匙还给你时,你自己的钥匙上也会多出他刻的名字。
你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里
这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。
为什么说Java 中只有值传递
一般认为Java内的传递都是值传递。
在Java中,方法的所有参数都是“传值”的,也就是说,方法中参数变量的值是调用者指定的值的拷贝。
例如,如果向方法的int型参数x传递一个int值,那么参数x得到的值是传递的值的拷贝。
很多人理解的是错误的
关于这个问题,在StackOverflow 上也引发过广泛的讨论,
看来很多程序员对于这个问题的理解都不尽相同,甚至很多人理解的是错误的。
还有的人可能知道Java 中的参数传递是值传递,但是说不出来为什么
错误理解一:值传递和引用传递,区分的条件是传递的内容
如果是个值,就是值传递。
如果是个引用,就是引用传递。
错误理解二:Java 是引用传递
错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。
求值策略
当进行方法调用时,需要把实际参数传递给形式参数,那么传递的过程中到底传递的是什么东西呢?
这其实是程序设计中求值策略(Evaluation strategies)的概念。
在计算机科学中,求值策略是确定编程语言中表达式的求值的一组(通常确定性的)规则。
求值策略定义何时和以何种顺序求值给函数的实际参数、什么时候把它们代换入函数、和代换以何种形式发生。
求值策略分为两大基本类,基于如何处理给函数的实际参数,分为
严格求值
在“严格求值”中,函数调用过程中,给函数的实际参数总是在应用这个函数之前求值。
多数现存编程语言对函数都使用严格求值。
所以,我们本文只关注严格求值。
在严格求值中有几个关键的求值策略是我们比较关心的
在严格求值中,有几个关键的求值策略
传值调用(Call by value)(值传递)
在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。
因为形式参数拿到的只是一个"局部拷贝",所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。
传值调用是指在调用函数时将实际参数复制一份传递到函数中
所以,两者的最主要区别就是是直接传递的,还是传递的是一个副本。
传引用调用(Call by reference)(引用传递)
在传引用调用中,传递给函数的是它的实际参数的隐式引用,而不是实参的拷贝。
因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。
传引用调用是指在调用函数时将实际参数的引用直接传递到函数中。
所以,两者的最主要区别就是是直接传递的,还是传递的是一个副本。
传共享对象调用(Call by sharing)(共享对象传递)
传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。
因为参数的地址都指向同一个对象,所以我们也称之为"传共享对象",
所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。
其实传共享对象调用和传值调用的过程几乎是一样的,都是进行"求值"、"拷贝"、"传递"。
但是,传共享对象调用和内传引用调用的结果又是一样的,都是在被调函数中如果改变参数的内容,那么这种改变也会对调用者有影响
共享对象传递和值传递以及引用传递之间到底有很么关系呢?
对于这个问题,我们应该关注过程,而不是结果,因为传共享对象调用的过程和传值调
用的过程是一样的,而且都有一步关键的操作,那就是"复制",所以,通常我们认为传共享
对象调用是传值调用的特例。
用的过程是一样的,而且都有一步关键的操作,那就是"复制",所以,通常我们认为传共享
对象调用是传值调用的特例。
非严格求值
Java 的求值策略
很多人说Java 中的基本数据类型是值传递的,这个基本没有什么可以讨论的,普遍都是这样认为的。
误区
但是,有很多人却误认为Java 中的对象传递是引用传递。
之所以会有这个误区,主要是因为Java 中的变量和对象之间是有引用关系的。
Java 语言中是通过对象的引用来操纵对象的。所以,很多人会认为对象的传递是引用的传递。
而且很多人还可以举出以下的代码示例:
可以看到,对象类型在被传递到pass 方法后,在方法内改变了其内容,最终调用方main 方法中的对象也变了。
所以,很多人说,这和引用传递的现象是一样的,就是在方法内改变参数的值,会影响到调用方。
但是,其实这是走进了一个误区。
Java 中的对象传递
很多人通过代码示例的现象说明Java 对象是引用传递,那么我们就从现象入手,先来反驳下这个观点。
,Java 中的求值策略是共享对象传递
Java 中只有值传递,只不过传递的内容是对象的引用。这也是没毛病的
绝对不能认为Java 中有引用传递。
有理数的类封装
有理数有两个重要的成员:分子和分母,另外还有重要的四则运算。
对象拷贝
为什么要使用克隆?
如何实现对象克隆?
深拷贝和浅拷贝区别是什么?
泛型
什么是泛型
在JDK1.5后
提出了一种新的概念,那就是泛型
Java集合框架开始支持泛型
泛型(Generics)是在JDK1.5中推出的
Generics(泛型)
泛型其实就是一种参数化的集合,它限制了你添加进集合的类型。
泛型的本质就是一种参数化类型。多态也可以看作是泛型的机制。
一个类继承了父类,那么就能通过它的父类找到对应的子类,但是不能通 过其他类来找到具体要找的这个类。
泛型的设计之处就是希望对象或方法具有最广泛的表达能力。
泛型诞生的背景
在集合中存储对象并在使用前进行类型转换是多么的不方便。
泛型防止了那种情况的发生。
它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
比如要写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,可以使用Java 泛型。
没有泛型的用法
这段程序不能正常运行,原因是Integer类型不能直接强制转换为String类型
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
用泛型进行改写后
示例代码如下
用泛型进行改写后
这段代码在编译期间就会报错,编译器会在编译阶段就能够发现类似这样的问题。
类型擦除
类型擦除是什么?
泛型是通过类型擦除来实现的
Java中的泛型基本上都是在编译器这个层次来实现的。
在生成的Java字节代码中是不包含泛型中的类型信息的。
使用泛型时加上的类型参数,会被编译器在编译时去掉。
泛型信息只存在编译阶段,在进入JVM之前,与泛型相关的信息会被擦除。
通俗来讲,泛型类和普通类在java虚拟机内没有什么特别的地方。
例如List<String>在运行时,仅用一个List来表示。
如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。
JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。
编译器在编译时,擦除了所有类型相关的信息,故在运行时不存在任何类型相关的信息。
这样做的目的
确保能和Java 5之前的版本开发二进制类库进行兼容。
你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。
类型擦除的基本过程
首先,找到用来替换类型参数的具体类。
这个具体类一般是Object。
如果指定了类型参数的上界,则使用这个上界。
把代码中的类型参数都替换成具体的类。
泛型的本质
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
参数化类型,就是将类型由原来的具体的类型参数化,就像方法中的变量参数,
此时类型也定义成参数形式(类型形参),然后在使用/调用时传入具体的类型(类型实参)
也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,
这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
使用泛型的目的与优点
主要目的:可以建立具有类型安全的集合框架/数据结构,如链表、散列映射等数据结构
帮助我们处理多种数据类型执行相同的代码
数据安全性
泛型中的类型在使用时指定类型后,不需要强制类型转换,
最早起Java是没有泛型的,它是使用Object来代替的,程序员在写程序时易出现类型转换的错误。
最重要的一个优点就是
在使用这些泛型类建立数据结构时,不必进行强制类型转换,即运行时不进行类型检查。
JDK1.5是支持泛型的编译器,它将运行时的类型检查提前到编译时执行,使代码更安全。
泛型的使用
泛型的使用有多种方式,下面我们就来一起探讨一下
用泛型表示类(泛型类)
可以使用“class 名称<泛型列表>”声明一个类
为了和普通的类有所区别,这样声明的类称作泛型类
泛型可以加到类上面,来表示这个类的类型
如:class ShowObject<E>
其中ShowObject是泛型类的名称
E是其中的泛型
并没有指定E是何种类型的数据,它可以是任何对象或接口,但不能是基本类型数据。
举例说明
用泛型表示类
使用泛型类声明对象
泛型类声明和创建对象时,类名后多了一对“<>”
而且必须要用具体的类型替换“<>”中的泛型
例如:
Cone<Circle> coneOne;
coneOne =new Cone<Circle>(new Circle());
coneOne =new Cone<Circle>(new Circle());
Java中如何使用泛型编写带有参数的类?
要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。
关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。
泛型类<T>
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。
一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
用泛型表示接口(泛型接口)
泛型接口与泛型类的定义及使用基本相同。
泛型类和普通类都可以实现泛型接口,但普通类实现泛型接口时,必须指定泛型接口中泛型列表中的具体类型
可以使用“interface 名称<泛型列表>”声明一个接口
这样声名的接口称作泛型接口
如:
interface Listen<E> {
void listen(E x);
}
void listen(E x);
}
其中Listen<E>是泛型接口的名称,E是其中的泛型。
用泛型表示接口
—般泛型接口常用于生成器中
生成器
相当于对象工厂
一种专门用来创建对象的类。
(generator)
使用泛型来表示方法(泛型方法)
可以使用泛型来表示方法
泛型方法(<E>)
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
使用泛型来表示方法(泛型方法)
如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
编写泛型方法并不困难,需要用泛型类型来替代原始类型
比如使用T, E or K,V等,被广泛认可的类型占位符。
泛型通配符/限定通配符与非限定通配符
List是泛型类,为了表示各种泛型List的父类,可以使用类型通配符,类型通配符使用问号(?)表 示,它的元素类型可以匹配任何类型。例如
例如
泛型通配符
泛型中的限定通配符
限定通配符对类型进行了限制。
泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
有两种限定通配符
上界通配符<? extends ClassType>
<? extends ClassType>
<? extends T>表示该通配符所代表的类型是T类型的子类。
<? extends T>,通过确保类型必须是T的子类来设定类型的上界
该通配符为ClassType的所有子类型。
它表示的是任何类型都是 ClassType类型的子类。
下界通配符<? super ClassType〉
<? super ClassType〉
<? super T>,通过确保类型必须是T的父类来设定类型的下界。
<? super T>表示该通配符所代表的类型是T类型的父类。
该通配符为ClassType的所有超类型。
它表示的是任何类型的父 类都是 ClassType。
List<? extends T>和List <? super T>之间有什么区别 ?
这两个List的声明都是限定通配符的例子,
List<? extends T>可以接受任何继承自T的类型的List
List<? super T>可以接受任何T的父类构成的List
泛型中的非限定通配符
<?>表示了非限定通配符,因为<?>可以用任意类型来替代。
例题
子主题
1. 只看尖括号里边的!!明确点和范围两个概念
2. 如果尖括号里的是一个类,那么尖括号里的就是一个点,比如List<A>,List<B>,List<Object>
3. 如果尖括号里面带有问号,那么代表一个范围,<? extends A> 代表小于等于A的范围,<? super A>代表大于等于A的范围,<?>代表全部范围
4. 尖括号里的所有点之间互相赋值都是错,除非是俩相同的点
5. 尖括号小范围赋值给大范围,对,大范围赋值给小范围,错。如果某点包含在某个范围里,那么可以赋值,否则,不能赋值
6. List<?>和List 是相等的,都代表最大范围
----------------------------------------------------------------------------------
7.补充:List既是点也是范围,当表示范围时,表示最大范围
2. 如果尖括号里的是一个类,那么尖括号里的就是一个点,比如List<A>,List<B>,List<Object>
3. 如果尖括号里面带有问号,那么代表一个范围,<? extends A> 代表小于等于A的范围,<? super A>代表大于等于A的范围,<?>代表全部范围
4. 尖括号里的所有点之间互相赋值都是错,除非是俩相同的点
5. 尖括号小范围赋值给大范围,对,大范围赋值给小范围,错。如果某点包含在某个范围里,那么可以赋值,否则,不能赋值
6. List<?>和List 是相等的,都代表最大范围
----------------------------------------------------------------------------------
7.补充:List既是点也是范围,当表示范围时,表示最大范围
子主题
ACDG 考察泛型通配符
?:任意类型,
如果没有明确,那么就是Object以及任意的Java类了
? extends E:
向下限定,E及其子类
? super E:
向上限定,E及其父类
类型通配符?
一般是使用?代替具体的类型参数。
例如 List<?> 在逻辑上是List<String>,List<Integer> 等所有List<具体类型实参>的父类。
常见问题
编写一段泛型程序来实现LRU缓存?
LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。
LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对
你可以把List<String>传递给一个接受List<Object>参数的方法吗?
因为List<Object>可以存储任何类型的对象包括String, Integer等等,
而List<String>却只能用来存储Strings。
List<Object> objectList;
List<String> stringList;
objectList = stringList; //compilation error incompatible types
List<String> stringList;
objectList = stringList; //compilation error incompatible types
Array中可以用泛型吗?
Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array
因为List可以提供编译期的类型安全保证,而Array却不能。
如何阻止Java中的类型未检查的警告?
如果你把泛型和原始类型混合起来使用
例如下列代码,Java 5的javac编译器会产生类型未检查的警告,
例如 List<String> rawList = new ArrayList()
枚举
枚举类型的诞生背景
在java 语言中还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具int常量。
参考代码
之前通常利用public final static 方法定义的代码如下,
分别用1 表示春天,2表示夏天,3 表示秋天,4 表示冬天。
这种方法称作int 枚举模式
存在的问题描述
可这种模式有什么问题呢,我们都用了那么久了,应该没问题的。
通常写出来的代码都会考虑它的安全性、易用性和可读性。
首先来考虑一下它的类型安全性。
当然这种模式不是类型安全的。
比如说设计一个函数,要求传入春夏秋冬的某个值。
但是使用int 类型,我们无法保证传入的值为合法。
代码示例
无法保证传入的值为合法
无法保证传入的值为合法
程序getChineseSeason ( Season .SPRING )是预期的使用方法。
可getChineseSeason(5)显然就不是了,而且编译很通过,在运行时会出现什么情况,就不得而知了。
这显然就不符合Java 程序的类型安全。
这种模式的可读性
使用枚举的大多数场合,我都需要方便得到枚举类型的字符串表达式。
如果将int 枚举常量打印出来,所见到的就是一组数字,这是没什么太大的用处。
我们可能会想到使用String 常量代替int 常量。
虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作,
所以这种模式也是不期望的。
从类型安全性和程序可读性两方面考虑,int 和String 枚举模式的缺点就显露出来了。
幸运的是,从Java1.5 发行版本开始,就提出了另一种可以替代的解决方案,可以避免int 和String 枚举模式的缺点,并提供了许多额外的好处。
那就是枚举类型(enum type)。
关于枚举
枚举可能是我们使用次数比较少的特性,
在Java中,枚举使用enum关键字来表示
枚举其实是一 项非常有用的特性,可以把它理解为具有特定性质的类。
总的来说,枚举的使用不是很复杂,它也是Java中很小的一块功能,但有时却能够因为这一个小技巧,能够让你的代码变得优雅和整洁。
enum不仅仅Java有,C和C++也有枚举 的概念。
Java中的枚举类型,采用关键字enum 来定义
从jdk1.5才有的新类型
所有的枚举类型,都是继承自Enum类型
Enum作为Sun全新引进的一个关键字,看起来很象是特殊的class,
它也可以有自己的变量,可以定义自己的方法,可以实现一个或者多个接口。
枚举类型的定义
枚举类型(enum type)
枚举类型
JDK1.5 引入了一种新的数据类型
是指由一组固定的常量组成合法的类型。
Java 中由关键字enum 来定义一个枚举类型。Java使用关键字enum声明枚举类型
Java 1.5 的枚举能满足绝大部分程序员的要求的,它的简明,易用的特点是很突出的。
Java SE5 提供了一种新的类型-Java 的枚举类型,
关键字enum 可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。
语法格式如下
enum 名字 {
常量列表
}
常量列表
}
其中的常量列表,是用逗号分割的字符序列,称为枚举类型的常量。
例如
enum Season {
spring, summer, autumn, winter
}
spring, summer, autumn, winter
}
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
SPRING, SUMMER, AUTUMN, WINTER;
}
声明了名字为Season的枚举类型,该枚举类型有4个常量。
枚举类型的好处
Java 定义枚举类型的语句很简约
可以避免int 和String 枚举模式的缺点,并提供了许多额外的好处
枚举类型的特点
1) 使用关键字enum ;
2) 类型名称,比如这里的Season ;
3) 一串允许的值,比如上面定义的春夏秋冬四季;
4) 枚举可以单独定义在一个文件中,也可以嵌在其它Java 类中除了这样的基本要求外,用户还有一些其他选择;
5) 枚举可以实现一个或多个接口(Interface);
6) 可以定义新的变量;
7) 可以定义新的方法;
8) 可以定义根据具体枚举值而相异的类。
它不能有public的构造函数,这样做可以保证客户代码没有办法新建一个enum的实例。
所有枚举值都是public , static,final的。
注意这一点只是针对于枚举值,可以和在普通类里面定义 变量一样定义其它任何类型的非枚举变量,这些变量可以用任何你想用的修饰符。
Enum默认实现了java.lang.Comparable接口。
枚举变量
enum Season {
spring, summer, autumn, winter
}
spring, summer, autumn, winter
}
声明了名字为Season的枚举类型,该枚举类型有4个常量。
声明了一个枚举类型后,就可以用该枚举类型声明一个枚举变量,该枚举变量只能取值枚举类型中的常量。
例如: Season x;
通过使用枚举名和“.”运算符获得枚举类型中的常量。
例如:x=Season.spring;
可以在一个Java源文件中只声明定义枚举类型,然后保存该源文件,然后单独编译这个源文件得到枚举类型的字节码文件,那么该字节码就可以被其他源文件中的类使用.
枚举类型与for语句和switch语句
1.使用for语句,遍历枚举常量
枚举类型可以用如下形式返回一个一维数组:
枚举类型的名字.values();
该一维数组元素的值和该枚举类型中常量依次相对应。
例如, WeekDay a[]=WeekDay.values();
那么,a[0]至a[6]的值依次为:星期一,星期二,星期三,星期四,星期五,星期六,星期日。
那么,a[0]至a[6]的值依次为:星期一,星期二,星期三,星期四,星期五,星期六,星期日。
2.switch语句中,使用枚举常量
JDK1.5后的版本,允许switch语句中表达式的值,是枚举类型的常量。
Java Enum 枚举特性,枚举特性与方法
toString() 方法
Enum覆载了toString方法,因此如果调用Color.Blue.toString()默认返回字符串”Blue”.
当创建完enum后,编译器会自动为你的enum添加toString() 方法
能够让你方便的显示enum实例的具体名字是什么
valueOf方法
Enum提供了一个valueOf方法
这个方法和toString方法是相对应的。
调用valueOf(“Blue”)将返回Color.Blue.
因此在自己重写toString方法时,就要注意到这一点,
一把来说应该相对应地重写valueOf方法。
values方法
这个方法使你能够方便的遍历所有的枚举值
1.遍历所有有枚举值. 知道了有values方法,我们可以轻车熟路地用ForEach循环来遍历了枚举值了。
for(Color c: Color.values())
System.out.println(“find value:” + c);
System.out.println(“find value:” + c);
ordinal()方法
编译器还会添加 ordinal()方法
用来表示enum常量的声明顺序,以及values,方法显示顺序的值。
这个方法返回枚举值在枚举类种的顺序,这个顺序根据枚举值声明的顺序而定,这里Color.Red.ordinal()返回0。
使用举例
使用举例
其他方法
子主题
枚举类型的应用场景
以在背景中提到的类型安全为例,用枚举类型重写那段代码。
枚举类
转换类
[中文:春天,枚举常量:SPRING,数据:1]
[中文:夏天,枚举常量:SUMMER,数据:2]
[中文:秋天,枚举常量:AUTUMN,数据:3]
[中文:冬天,枚举常量:WINTER,数据:4]
为什么我要将域添加到枚举类型中呢?
目的是想将数据与它的常量关联起来。如1 代表春天,2 代表夏天。
什么时候应该使用枚举呢?
每当需要一组固定的常量时,如一周的天数、一年四季等。
或者是在编译前,就知道其包含的所有值的集合。
用法一:常量
public enum Color {
RED, GREEN, BLANK, YELLOW
}
RED, GREEN, BLANK, YELLOW
}
用法二:switch
JDK1.5后的版本,允许switch语句中表达式的值,是枚举类型的常量。
用法二:switch
用法三:向枚举中添加新方法
用法三:向枚举中添加新方法
用法四:覆盖枚举的方法
用法四:覆盖枚举的方法
用法五:实现接口
用法五:实现接口
用法六:使用接口组织枚举
用法六:使用接口组织枚举
枚举的实现与源码分析
要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?
答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢
简单的写一个枚举
反编译后代码内容如下:
反编译后代码内容如下:
通过反编译后代码我们可以看到,
public final class T extends Enum,
该类是继承了Enum类的,
final关键字,这个类也是不能被继承的。
当使用enmu 来定义一个枚举类型时,编译器会自动创建一个final类型的类继承Enum 类,所以枚举类型不能被继承。
下面是一个枚举的例子
创建了一个Family的枚举类,
枚举的例子
它具有4个值,由于枚举类型都是常量,所以都用大写字母 来表示。
通常定义常量方法
package com.csdn.myEnum;
public class Light {
public final static int RED =1; /* 红灯 */
public final static int GREEN =3; /* 绿灯 */
public final static int YELLOW =2; /* 黄灯 */
}
public class Light {
public final static int RED =1; /* 红灯 */
public final static int GREEN =3; /* 绿灯 */
public final static int YELLOW =2; /* 黄灯 */
}
枚举类型定义常量方法
枚举类型的简单定义,方法如下,似乎没办法定义每个枚举类型的值。
public enum Light {
RED , GREEN , YELLOW ;
}
public enum Light {
RED (1), GREEN (3), YELLOW (2); // 利用构造函数传参
private int nCode ; // 定义私有变量
private Light( int _nCode) { // 构造函数,枚举类型只能为私有
this . nCode = _nCode;
}
public String toString() {
return String.valueOf ( this . nCode );
}
}
RED , GREEN , YELLOW ;
}
public enum Light {
RED (1), GREEN (3), YELLOW (2); // 利用构造函数传参
private int nCode ; // 定义私有变量
private Light( int _nCode) { // 构造函数,枚举类型只能为私有
this . nCode = _nCode;
}
public String toString() {
return String.valueOf ( this . nCode );
}
}
还有遍历问题。。switch也可以使用
那么enum创建出来了,该如何引用呢?
如何引用枚举?
枚举的静态导入包
enum可以进行静态导入包
静态导入包可以做到不用输入枚举类名.常量,可以直接使用常量
使用ennum和static关键字,可以做到静态导入包
静态导入包
上面代码导入的是Family中所有的常量,也可以单独指定常量。
枚举和普通类一样
定义常量
除了枚举中能够方便快捷的定义常量,
定义public static final xxx
日常开发使用的public static final xxx其实都可以用枚举来定义。
定义属性和方法
在枚举中也能够定义属性和方法
千万不要把它看作是异类, 它和万千的类一样。
举例使用
举例使用
和switch连用,构造一个小型的状态转换机
举例使用
是不是代码顿时觉得优雅整洁了些许呢?
枚举神秘之处
在Java中,万事万物都是对象,enum虽然是个关键字,但是它却隐式的继承于Enum类。
Enum类
此类位于java.lang包下,可以.自动引用。
Enum类
此类的属性和方法都比较少。
这个类中没有values方法,values方法是使用枚举时,被编译器添加进来的static方法。可以使用反射来验证一下。
除此之外,enum还和Class类有交集,在Class类中有三个关于Enum的方法
getEnumConstants(): T[]
获取enum常量
getEnumConstantsShared(): T[]
获取enum常量
isEnum(): boolean
判断是否是枚举类型的
两个关于枚举的工具类
除了 Enum外,还需要知道两个关于枚举的工具类
EnumSet
JDK1.5引入的
设计充分考虑到了速度因素,使用EnumSet可以作为Enum的替代者,因为它的效率比较高。
EnumMap
一种特殊的Map,它要求其中的key键值是来自一个enum。
因为EnumMap速度也很快,可以使用EnumMap作为key的快速查找。
内部类
是什么?
一种有用而且骚气的定义类的方式
内部类的定义非常简单:可以将一个类的定义放在另一个类的内部,这就是内部类。
内部类是一种非常有用的特性,定义在类内部的类,持有外部类的引用,但却对其他外部类不可见,看 起来就像是一种隐藏代码的机制,
就和《海贼王》弗兰奇将军似的,弗兰奇可以和弗兰奇将军进行通讯,但是外面的敌人却无法直接攻击到弗兰奇本体。
Java支持在一个类中声明另一个类,这样的类称作内部类,而包含内部类的类成为内部类的外嵌类。
Java类中
可以定义变量和方法
可以定义类
定义在类内部的类就被称为内部类。
根据定义的方式不同,内部类分为四种
静态内部类
定义在类内部的静态类
访问外部类所有的静态变量和方法,即使是private的也一样。
和一般类一致,可以定义静态变量、方法,构造方法等。
其它类使用静态内部类需要使用“外部类.静态内部类”方式,
如下所示:Out.Inner inner = new Out.Inner();inner.print();
Java集合类HashMap内部就有一个静态内部类Entry
Entry是HashMap存放元素的抽象
HashMap内部维护Entry数组用了存放元素,但是Entry对使用者是透明的。
像这种和外部类关系密切的,且不依赖外部类实例的,都可以使用静态内部类。
成员内部类
定义在类内部的非静态类
成员内部类不能定义静态方法和变量(final修饰的除外)
这是因为成员内部类是非静态的,类初始化时先初始化静态成员
如果允许成员内部类定义静态变量,那么成员内部类的静态变量初始化顺序是有歧义的。
局部内部类
定义在方法中的类,就是局部类。
如果一个类只在某个方法中使用,则可以考虑使用局部类。
匿名内部类
要继承一个父类或者实现一个接口、直接使用new来生成一个对象的引用
必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口。
同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引用。
创建内部类的方式
定义内部类非常简单,就是直接将一个类定义在外围类的里面
如下代码所示
创建内部类
在这段代码中,InnerClass就是OuterClass的一个内部类。
每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
这 也是隐藏了内部实现细节。内部类拥有外部类的访问权。
定义内部类的方式
内部类,不仅仅能够定义在类的内部,还可以定义在方法和作用域内部,这种被称为局部内部类,
除此 之外,还有匿名内部类、内部类可以实现Java中的 多重继承。
・一个在方法中定义的类(局部内部类)
・一个定义在作用域内的类,这个作用域在方法的内部(成员内部类)
・一个实现了接口的匿名类(匿名内部类)
・一个匿名类,它扩展了非默认构造器的类
・一个匿名类,执行字段初始化操作
・一个匿名类,它通过实例初始化实现构造
・一个定义在作用域内的类,这个作用域在方法的内部(成员内部类)
・一个实现了接口的匿名类(匿名内部类)
・一个匿名类,它扩展了非默认构造器的类
・一个匿名类,执行字段初始化操作
・一个匿名类,它通过实例初始化实现构造
编译后的内部类
由于每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息,
如何表示内部类的信息呢?
可以使用$来表示,比如OuterClass$lnnerClass.class。
匿名类
和子类有关的匿名类
Java允许直接使用一个类的子类的类体创建一个子类对象。
创建子类对象时,除了使用父类的构造方法外还有类体,此类体被认为是一个子类去掉类声明后的类体,称作匿名类。
假设Bank是类,那么下列代码就是用Bank的一个子类(匿名类)创建对象
new Bank () {
匿名类的类体
};
匿名类的类体
};
匿名类的常用的方式是向方法的参数传值。
void f(A a){ }
和接口有关的匿名类
假设Computable是一个接口,那么,Java允许直接用接口名和一个类体创建一个匿名对象,
此类体被认为是实现了Computable接口的类去掉类声明后的类体,称作匿名类。
此类体被认为是实现了Computable接口的类去掉类声明后的类体,称作匿名类。
new Computable() {
实现接口的匿名类的类体
} ; //用实现了Computable接口的类(匿名类)创建对象:
实现接口的匿名类的类体
} ; //用实现了Computable接口的类(匿名类)创建对象:
如果某个方法的参数是接口类型,那么可以使用接口名和类体组合创建一个匿名对象传递给方法的参数,对于
void f(ComPutable x) 其中的参数x是接口,那么在调用f时,可以向f的参数x传递一个匿名对象,
void f(ComPutable x) 其中的参数x是接口,那么在调用f时,可以向f的参数x传递一个匿名对象,
例如:
f(new ComPutable() {
实现接口的匿名类的类体
})
f(new ComPutable() {
实现接口的匿名类的类体
})
常见问题
内部类可以引用它包含的类的成员吗?如果可以,有什么限制?
一个内部类对象可以访问创建他的外部类对象的内容。
内部类不是static的,那么它可以访问创建他的外部类对象的所有属性。
内部类是static的,那么它可以访问创建他的外部类对象的static属性。
数组
数组是什么?
属于引用型变量
相同类型的变量,按顺序组成的一种复合数据类型
称这些相同类型的变量为数组的元素或单元
数组通过数组名加索引,来使用数组的元素。
创建数组,需经过两个步骤
声明数组
声明数组包括
数组变量的名字(简称数组名)
数组类型
两种格式声明【一维数组】
数组的元素类型 数组名[];
数组的元素类型 [] 数组名;
例如:float boy[];
两种格式声明【二维数组】
数组的元素类型 数组名[][];
数组的元素类型 [][] 数组名;
例如:char cat[][];
注:与C/C++不同,Java不允许在声明数组中的方括号内,指定数组元素的个数。
若声明: int a[12]; 或 int [12] a; 将导致语法错误。
为数组分配变量
为数组分配元素【一维数组】
声明数组后,还必须为它分配内存空间,即创建数组。
为一维数组分配内存空间的格式如下:
数组名字 = new 数组元素的类型[数组元素的个数];
例如:float boy[]; //声明数组
boy= new float[4];
boy= new float[4];
声明数组和创建数组可以一起完成
例如: float boy[]=new float[4];
数组的内存模型
数组的内存模型
为数组分配元素【二维数组】
二维数组和一维数组一样,在声明之后必须用new运算符为数组分配内在空间。
例如:int mytwo[][];
mytwo = new int [3][4];
mytwo = new int [3][4];
声明数组和创建数组可以一起完成
例如: int mytwo[][] = new int[3][4];
上述创建的二维数组mytwo就是由3个长度为4的一维数组:mytwo[0]、mytwo[1]和mytwo[2]构成的。
Java采用“数组的数组”来声明多维数组.
构成二维数组的一维数组不必有相同的长度,在创建二维数组时,可以分别指定构成该二维数组的一维数组的长度
例如: int a[][]=new int[3][];
注:和C语言不同的是,Java允许使用int型变量的值,指定数组的元素的个数
例如:
int size=30;
double number[]=new double[size];
int size=30;
double number[]=new double[size];
数组元素的使用
一维数组,通过索引符,访问自己的元素,索引从0开始 。
如boy[0],boy[1]等。
二维数组,通过索引符,访问自己的元素,索引从0开始。
如a[0][1],a[1][2]等;
举例
比如,声明创建了一个二维数组a: int a[][] = new int[2][3];
那么第一个索引的变化范围为从0到1,第二个索引变化范围为从0到2。
length的使用
数组元素的个数,称作数组的长度。
对于一维数组,“数组名.length”的值,就是数组中元素的个数。
对于二维数组,“数组名.length”的值,是它含有的一维数组的个数。
例如
float a[] = new float[12];
a.length的值12;
int b[][] = new int[3][6];
b.length的值是3。
数组的初始化
创建数组后,系统会给数组的每个元素一个默认值
如,float型是0.0
在声明数组的同时,也可以给数组元素一个初始值
如:float boy[] = { 21.3f,23.89f,2.0f,23f,778.98f};
也可以直接用若干个一维数组初始化一个二维数组,这些一维数组的长度不尽相同
例如: int a[][]= {{1}, {1,1},{1,2,1}, {1,3,3,1},{1,4,6,4,1}};
数组的引用
数组属于引用型变量,因此,两个相同类型的数组,如果具有相同引用,它们就有完全相同元素。
例如
对于int a[] = {1,2,3}, b[ ]= {4,5}; 数组变量a和b分别存放着引用0x35ce36和0x757aef。
数组a 、b的内存模型
如果使用了下列赋值语句(a和b的类型必须相同) a=b;
a = b 后的数组a、b的内存模型
那么,a中存放的引用和b的相同,这时系统将释放最初分配给数组a的元素,使得a的元素和b的元素相同。
a、b的内存模型变成如图7.2,7.3所示。
遍历数组
基于循环语句的遍历
语法格式如下:
for(声明循环变量:数组的名字) {
… …
}
… …
}
其中,声明的循环变量的类型,必须和数组的类型相同。
可以将这种形式的for语句中翻译成“对于循环变量依次取数组的每一个元素的值”。
使用toString()方法遍历数组
让Arrays类调用toString()方法,可以得到参数指定的一维数组a的如下格式的字符串表示:[a[0],a[1] …a[a.length-1]]
例如,对于数组: int []a = {1,2,3,4,5,6}; Arrays.toString(a)得到的字符串是 [1,2,3,4,5,6]
public static String toString(int[] a)方法
复制数组
arraycopy方法
利用循环语句可以把一个数组的元素的值分别赋值给另一个数组中相应的元素。
Java提供的更简练的数组之间的快速复制。
System类调用方法
public static void arraycopy(sourceArray, int index1, copyArray,int index2,int length)
可以将数组sourceArray,从索引index1开始后的length个元素中的数据,复制到数组copyArray。
copyOf方法
Arrays类调用copyOf方法
public static double[] copyOf(double[] original, int newLength)
可以把参数original指定的数组中,
从索引0开始的newLength个元素,复制到一个新数组中,
并返回这个新数组,且该新数组的长度为newLength。
类似的方法还有:
public static float[] copyOf(float[] original,int newLength)
public static int[] copyOf(int[] original,int newLength)
public static char[] copyOf(char[] original,int newLength)
public static int[] copyOf(int[] original,int newLength)
public static char[] copyOf(char[] original,int newLength)
例如
int [] a={100,200,300,400};
int [] b=Arrays.copyOf(a,5);
int [] b=Arrays.copyOf(a,5);
那么b[0]=100,b[1]=200,b[2]=300,b[3]=400,b[4]=0。
即b的长度为5,最后一个元素b[4]取默认值0。
copyOfRange()方法
把数组中部分元素的值,复制到另一个数组中
例如:
public static double[] copyOfRange(double[] original,int from,int to)
public static float[] copyOfRange(flaot[] original,int from,int to)
public static int[] copyOfRange(int[] original,int from,int to)
public static char[] copyOfRange(char[] original,int from,int to)
public static double[] copyOfRange(double[] original,int from,int to)
public static float[] copyOfRange(flaot[] original,int from,int to)
public static int[] copyOfRange(int[] original,int from,int to)
public static char[] copyOfRange(char[] original,int from,int to)
排序与二分查找
Arrays类调用相应的方法,可以实现对数组的快速排序。
public static void sort(double a[])
把参数a指定的double类型数组按升序排序。
public static void sort(double a[],int start,int end)
把参数a指定的double类型数组中索引star至end-1的元素的值按升序排序。
public static int binarySearch(double[] a, double number)
判断参数number指定的数值是否在参数a指定的数组中(二分法)。
如果number和数组a中某个元素的值相同,该方法返回(得到)该元素的索引,否则返回一个负数。
断言
断言的作用
断言语句,用于调试代码阶段。
在调试代码阶段,让断言语句发挥作用,这样就可以发现一些致命的错误,
当程序正式运行时就可以关闭断言语句,但仍把断言语句保留在源代码中,如果以后应用程又需要调试,可以重新启用断言语句。
使用方法
使用关键字assert声明一条断言语句
断言语句有以下两种格式
assert booleanExpression;
assert booleanExpression:messageException;
assert booleanExpression:messageException;
使用java解释器直接运行应用程序时,默认地关闭断言语句,在调试程序时,可以使用-ea,启用断言语句,例如: java -ea mainClass
常用类
String、StringBuffer和StringBuilder
恒定的String对象
String类是什么?
常见,常用,常考
String 是Java 中一个比较基础的类,每一个开发人员都会经常接触到
Java 中最常用的一个数据类型了。
面试中经常会考的知识点
字符串大家一定都不陌生,他是我们非常常用的一个类。
String表示的就是Java中的字符串
String类在java.lang包中。
String 不是基本数据类型,是引用类型。
Java中最重要的类,提供了各种构造和管理字符串的操作;
用来定义字符串常量
String类用来创建一个字符串变量,字符串变量是对象。
日常开发用到的使用“”双引号包围的数都是字符串的实例。
典型的字符串的声明
上面你创建了一个名为abc的字符串,
两种创建字符串的方式,构造字符串对象的方式
常量对象(字面量)/字符串常量对象
用双引号括起的字符序列
例如
"你好"、"12.97"、"boy"
String str = "Hollis";
字符串对象
声明
String s;
如何创建:直接new一个String
s=new String("we are students");
String str = new String("Hollis");
如何创建:赋值,用一个已创建的字符串创建另一个字符串
例如: String tom=String(s);
引用字符串常量对象
string s1,s2;
s1 = "how are you";
s2 = "how are you";
s1 = "how are you";
s2 = "how are you";
这样,s1,s2具有相同的引用,因而具有相同的实体。
s1,s2具有相同的引用
String字符串是恒定不可变的, 一旦创建出来就不会被修改
Immutable类
String类就是一个典型的Immutable类
它是Immutable类,即不可变,原生的保证了线程安全;
String类是典型的Immutable不可变类实现
保证了线程安全性
一旦创建出来,就不会被修改
String字符串是恒定的, 一旦创建出来就不会被修改
String 是Java 中一个不可变的类,所以一旦被实例化就无法被修改。
一旦一个string 对象在内存(堆)中被创建出来,他就无法被修改。
如果你需要一个可修改的字符串,应该使用StringBuffer 或者StringBuilder。
不可变类的实例一旦创建,其成员变量的值就不能被修改
这样设计有很多好处
可缓存hashcode、
使用更加便利
使用更加安全
String是不可变的对象, 因此在每次对String 类型进行改变的时候,都会生成一个新的 String 对象,
然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,
因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。
修改后会产生新的String对象
由于String的不可变性, 类似字符串拼接、字符串截取等操作都会产生新的String对象
所有对String字符串的修改都会构造出一个新的String对象
注意:String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string 对象被创建出来。
String的构造和各种操作都会产生一个新的String对象,需考虑内存和垃圾回收问题
String相关源码的声明
String源码的声明
String对象是由final修饰的
String不仅仅它的类是final的, 它类里面的方法也是由final修饰的
图解字符串的不可变性
定义一个字符串
String s = "abcd";
s 中保存了string 对象的引用。下面的箭头可以理解为“存储他的引用”。
使用变量来赋值变量
String s2 = s;
s2 保存了相同的引用值,因为他们代表同一个对象。
字符串连接
s = s.concat("ef");
s 中保存的是一个重新创建出来的string 对象的引用。
String的方法总览
String 有很多方法,有些方法比较常用,有些方法不太常用
public String substring(int startpoint):
对字符串进行截取
获得一个当前字符串的子串
public String substring(int beginIndex, int endIndex)
为简单起见,后文中用substring()代表substring(int beginIndex, int endIndex)方法。
substring() 的作用
substring(int start ,int end)
substring(int beginIndex, int endIndex)方法
截取字符串并返回其[beginIndex,endIndex-1]范围内的内容。
举例
String tom = "我喜欢篮球";
String s = tom.substring(1,3);
String s = tom.substring(1,3);
那么s是:"喜欢"
输出内容:bc
调用substring()时发生了什么?
因为x 是不可变的,当使用x.substring(1,3)对x 赋值时,它会指向一个全新的字符串
指向一个全新的字符串
然而,这个图不是完全正确的表示堆中发生的事情。
因为在jdk6 和jdk7 中,调用substring 时发生的事情并不一样。
注意:substring在不同版本的JDK 中的实现是不同的
了解他们的区别可以帮助你更好的使用他
JDK 6 中的substring,使用不当,字符数组一直在被引用,无法被回收会导致内存泄露的问题
String是通过字符数组实现的。
String类包含三个成员变量
char value[]
存储真正的字符数组
int offset
数组的第一个位置索引
int count
字符串中包含的字符个数
当调用substring 方法时,会创建一个新的string对象,但是这个string 的值仍然指向堆中的同一个字符数组。
这两个对象中只有count 和offset 的值是不同的。
下面是证明上说观点的Java 源码中的关键代码
Java 源码中的关键代码
JDK 6 中的substring 导致的问题
如果你有一个很长很长的字符串,但是当你使用substring 进行切割时,你只需要很短的一段。
这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串
(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。
在JDK 6 中,一般用以下方式来解决该问题,原理其实就是生成一个新的字符串并引用他。
x = x.substring(x, y) + ""
关于JDK 6 中subString 的使用不当会导致内存泄露已经被官方记录在Java BugDatabase 中:
Java BugDatabase
内存泄露:在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使
用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设
计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设
计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
JDK 7 中的substring,创建了一个新字符串,避免对老字符串的引用,从而解决了内存泄露问题
上面提到的问题,在jdk 7 中得到解决。在jdk 7 中,substring 方法会在堆内存中创建一个新的数组。
在jdk 7 中,substring 方法会在堆内存中创建一个新的数组。
Java 源码中关于这部分的主要代码如下:
Java 源码中关于这部分的主要代码如下:
以上是JDK 7 中的subString 方法,其使用new String 创建了一个新字符串,避免对老字符串的引用。从而解决了内存泄露问题。
所以,如果生产环境中使用的JDK 版本小于1.7,使用String 的subString方法时,一定要注意,避免内存泄露。
public int length()
获取一个字符串的长度
String china = "欢度60周年国庆";
int n1,n2;
n1 = china.length();
n2 = "字母abc".length();
int n1,n2;
n1 = china.length();
n2 = "字母abc".length();
public boolean equals(String s)
用于判断String对象的值是否相等
比较当前字符串对象的实体是否与参数s指定的字符串的实体相同
String tom = new String("天道酬勤");
String boy = new String( "知心朋友");
String jerry = new String("天道酬勤");
String boy = new String( "知心朋友");
String jerry = new String("天道酬勤");
注:tom == jerry的值是false
内存示意图
public boolean equalsIgnoreCase(String s)
public boolean startsWith(String s) 、 public boolean endsWith(String s)
判断当前字符串对象的前缀(后缀)是否参数s指定的字符串
String tom = "天气预报,阴有小雨",jerry = "比赛结果,中国队赢得胜利";
tom.startsWith("天气") true / jerry.startsWith("天气") false
tom.endsWith("大雨") false / jerry.endsWith("胜利") true
public int compareTo(String s)
按字典序与参数s指定的字符串比较大小
public int compareToIgnoreCase(String s)
String str = "abcde";
str.compareTo("boy")小于0
str.compareTo("aba")大于0
str.compareTo("abcde")等于0
str.compareTo("boy")小于0
str.compareTo("aba")大于0
str.compareTo("abcde")等于0
public boolean contains(String s)
判断当前字符串对象是否含有参数指定的字符串s
是否包含指定字符序列
tom="student";
tom.contains("stu")的值就是true;
tom.contains("ok")的值是false。
public int indexOf (String s)
用于检索字符串
从当前字符串的头开始检索字符串s,并返回首次出现s的位置
类似API
indexOf(String s ,int startpoint)
lastIndexOf (String s)
String tom = "I am a good cat";
tom.indexOf("a");//值是2
tom.indexOf("good",2);//值是7
tom.indexOf("a",7);//值是13
tom.indexOf("w",2);//值是-1
tom.indexOf("a");//值是2
tom.indexOf("good",2);//值是7
tom.indexOf("a",7);//值是13
tom.indexOf("w",2);//值是-1
public String trim()
去掉多余空格
得到一个s去掉前后空格后的字符串对象
其他方法
charAt:返回指定位置上字符的值
concat:用于字符串拼接, 效率高于+
join:字符串拼接
String的方法equals与==的区别
equals
比较的是值。
用来比较两个对象内部的内容是否相等的。
equals方法没有重写还是比较对象地址。
重写equals方法后还得看是如何重写的。
==
是用来判断两个对象的地址是否相同,即是否是指相同一个对象。
用于基本数据类型比较的是值
用于包装类(引用类)比较的是对象地址。
比较的是对象应用
请说明“==”比较的是什么?
如果两个对象完全相同时,“==”将返回true,
“==”两边是基本类型,就比较数值是否相等。
若对一个类不重写,equal()方法是如何比较的?
比较的是对象的地址。
equals没重写时候和==一样,比较的是对象的地址,题中new 了两个对象,所以各自地址不一样,使用equals比较为false
Object 中euqals的源码如上
public boolean equals(Object obj) {
return (this == obj);
}
return (this == obj);
}
没有重写equals时,是直接用==判断的,而String中重写了equals方法。
String类型中的equals方法
Java默认重写了,可以比较对象里的值;
两个对象指向的同一个string成员变量里的值相同,所以equals比较也相同。
String类重写了equlas方法,类型不同返回false,附上源码(jdk1.7)
子主题
正则表达式及字符串的匹配、替换与分解
正则表达式
一个正则表达式是含有一些具有特殊意义字符的字符串,这些特殊字符称作正则表达式中的元字符。
比如,“\\dhello”中的\\d就是有特殊意义的元字符,代表0到9中的任何一个。
元字符
元字符
限定符
限定符
字符串的匹配(match)
public boolean matches(String regex)
match:正则表达式的字符串匹配
String类的match方法
字符串对象调用matches方法,可以判断当前字符串对象是否和参数regex指定的正则表达式匹配。
字符串的替换(replace)
replace、replaceAll 和replaceFirst 是Java 中常用的替换字符的方法,
replace(CharSequence target, CharSequence replacement)
用replacement 替换所有的target,两个参数都是字符串。
replace()替换字符串
replace:用于字符串替换
System.out.println("abac".replace("a", "\a")); //\ab\ac
replaceAll(String regex, String replacement)
用replacement 替换所有的regex 匹配项,regex 很明显是个正则表达式,replacement 是字符串。
字符串对象调用replaceAll方法返回一个字符串,
该字符串是当前字符串中。
所有和参数regex指定的正则表达式匹配的子字符串,
被参数replacement指定的字符串替换后的字符串
替换符合正则的所有文字
替换的内容不同,替换所有匹配的字符
代码
String result="12hello567".replaceAll("\\d+","你好");
那么result就是:“你好hello你好”
替换的内容不同,替换所有匹配的字符
替换的内容不同,替换所有匹配的字符
replaceAll()替换所有html 标签
replaceAll()替换所有html 标签
replaceAll() 替换指定文字
replaceAll() 替换指定文字
replaceFirst(String regex, String replacement)
基本和replaceAll 相同,区别是只替换第一个匹配项。
替换第一个符合正则的数据
替换的内容不同,仅替换第一次出现的字符。
代码
用法例子
以下例子参考:http://www.51gjie.com/java/771.html
字符串的分解
public String[] split(String regex)
split:字符串分割
字符串调用split使用参数指定的正则表达式regex做为分隔标记分解出其中的单词,并将分解出的单词存放在字符串数组中。
例如
对于字符串 str=“1931年09月18日晚, 日本发动侵华战争, 请记住这个日子!”;
使用正则表达式:String regex="\\D+";
做为分隔标记分解出str中的单词: String digitWord[]=str.split(regex);
那么,digitWord[0]、digitWord[1]和digitWord[2]就分别是"1931"、"09"和"18"。
使用String.split分割字符串对比与StringTokenizer分割字符串
使用String.split分割字符串,注意处理分隔符中的一些特殊字符
java.lang.String 的 split() 方法, JDK 1.4 or later
异常的代码示例
public String[] split(String regex,int limit)
public class StringSplit {
public static void main(String[] args) {
String sourceStr = "1,2,3,4,5";
String[] sourceStrArray = sourceStr.split(",");
for (int i = 0; i < sourceStrArray.length; i++) {
System.out.println(sourceStrArray[i]);
}
// 最多分割出3个字符串
int maxSplit = 3;
sourceStrArray = sourceStr.split(",", maxSplit);
for (int i = 0; i < sourceStrArray.length; i++) {
System.out.println(sourceStrArray[i]);
}
}
}
public class StringSplit {
public static void main(String[] args) {
String sourceStr = "1,2,3,4,5";
String[] sourceStrArray = sourceStr.split(",");
for (int i = 0; i < sourceStrArray.length; i++) {
System.out.println(sourceStrArray[i]);
}
// 最多分割出3个字符串
int maxSplit = 3;
sourceStrArray = sourceStr.split(",", maxSplit);
for (int i = 0; i < sourceStrArray.length; i++) {
System.out.println(sourceStrArray[i]);
}
}
}
预期外的输出结果:
1
2
3
4
5
1
2
3,4,5
2
3
4
5
1
2
3,4,5
进一步解释说明
split 的实现直接调用的 matcher 类的 split 的方法。
在使用String.split方法分隔字符串时,分隔符如果用到一些特殊字符,可能会得不到预期的结果。
在正则表达式中有特殊的含义的字符,使用时,必须进行转义,
转义后的代码示例:
public class StringSplit {
public static void main(String[] args) {
String value = "192.168.128.33";
// 注意要加\\,要不出不来,yeahString[] names = value.split("\\.");
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
}
}
public static void main(String[] args) {
String value = "192.168.128.33";
// 注意要加\\,要不出不来,yeahString[] names = value.split("\\.");
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
}
}
split分隔符总结
字符"|","*","+"都得加上转义字符,前面加上"\\"。
而如果是"\",那么就得写成"\\\\"。
如果一个字符串中有多个分隔符,可以用"|"作为连字符。
比如:String str = "Java string-split#test",可以用Str.split(" |-|#")把每个字符串分开。这样就把字符串分成了3个子字符串。
使用StringTokenizer分割字符串
java.util.Tokenizer JDK 1.0 or later
StringTokenizer 类允许应用程序将字符串分解为标记。
StringTokenizer 是出于兼容性的原因,而被保留的遗留类。
(虽然在新代码中并不鼓励使用它)
建议所有寻求此功能的人使用 String 的 split 方法或 java.util.regex 包。
样例代码
public class StringSplit {
public static void main(String[] args) {
String ip = "192.168.128.33";
StringTokenizer token=new StringTokenizer(ip,".");
while(token.hasMoreElements()){
System.out.print(token.nextToken()+" ");
}
}
}
public static void main(String[] args) {
String ip = "192.168.128.33";
StringTokenizer token=new StringTokenizer(ip,".");
while(token.hasMoreElements()){
System.out.print(token.nextToken()+" ");
}
}
}
但是StringTokenizer对于字符串"192.168..33"的分割,返回的字符串数组只有3个元素,对于两个分隔符之间的空字符串会忽略,这个要慎重使用。
但是String.split用的都是按顺序遍历的算法,时间复杂度O(m*n)较高
(String.split是用正则表达式匹配,所以不使用KMP字符串匹配算法)
性能上,StringTokenizer好很多,对于频繁使用字符串分割的应用,例如etl数据处理,使用StringTokenizer性能可以提高很多。
字符串与字符、字节数组
public void getChars(int start,int end,char c[],int offset )
复制String中的字符到指定的数组
String类提供了将字符串存放到数组中的方法
public char[] toCharArray()
把String对象转换为字符数组
将字符串中的全部字符存放在一个字符数组中的方法
字符串的加密算法
利用字符串和数组的关系,使用一个字符串password作为密码对另一个字符串sourceString进行加密
操作过程如下
将密码password存放到一个字符数组中
char [] p=password.toCharArray();
EncryptAndDecrypt
最后,将字符数组c转化为字符串得到sourceString的密文。
public byte[] getBytes()
使用平台默认的字符编码,将当前字符串转化为一个字节数组
public byte[] getBytes(String charsetName)
使用参数指定字符编码,将当前字符串转化为一个字节数组。
(必须在try~catch语句中调用此方法 )
字符串与基本数据类型的相互转化
将由“数字”字符组成的字符串,转化为相应的基本数据类型
java.lang包中的Integer类调用其类方法public static int parseInt(String s)可以将由“数字”字符组成的字符串
如"12356",转化为int型数据
例如:
int x;
String s = "876";
x = Integer.parseInt(s);
int x;
String s = "876";
x = Integer.parseInt(s);
类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。
有三种方式将一个int类型的变量变成String 类型
int i = 5;
String i1 = "" + i;
等价与String i1 = (new StringBuilder()).append(i).toString();
首先,创建一个StringBuilder 对象,
然后,再调用append 方法,
最后,调用toString 方法。
String i2 = String.valueOf(i);
public static String valueOf(byte n)
把对象转换为字符串
可以使用String 类的valueOf方法,将形如123、1232.98等数值转化为字符串。
String.valueOf(i)也是调用Integer.toString(i)来实现的
String i3 = Integer.toString(i);
本质上和String.valueOf(i);一样
将字符数组转换为字符串
方式一:静态方法:static String copyValueOf(char[] data)
静态方法:static String copyValueOf(char[] data)
返回指定数组中表示该字符序列的String。
返回指定数组中表示该字符序列的String。
ch=ch.copyValueOf(arr);
System.out.println(ch);
方式二:构造方法:String(char[] value)
构造方法:String(char[] value)
分配一个新的 String,使其表示字符数组参数中当前包含的字符序列
分配一个新的 String,使其表示字符数组参数中当前包含的字符序列
String result=new String (arr);
System.out.println(result);
方式三:static String valueOf(char[] data)
static String valueOf(char[] data)
返回 char 数组参数的字符串表示形式。
返回 char 数组参数的字符串表示形式。
mString = mString.valueOf(mArray);
判断String是否包含子串的四种方法及性能对比
判断String是否包含子串的四种方法
JDK原生方法String.indexOf
在String的函数中,提供了indexOf(subStr)方法,返回子串subStr第一次出现的位置,如果不存在则返回-1。
//包含Java
assertEquals(7, "Pkslow Java".indexOf("Java"));
//如果包含多个,返回第一次出现位置
assertEquals(0, "Java Java".indexOf("Java"));
//大小写敏感
assertEquals(-1, "Google Guava".indexOf("guava"));
assertEquals(7, "Pkslow Java".indexOf("Java"));
//如果包含多个,返回第一次出现位置
assertEquals(0, "Java Java".indexOf("Java"));
//大小写敏感
assertEquals(-1, "Google Guava".indexOf("guava"));
JDK原生方法String.contains
最直观判断的方法是contains(subStr),返回类型为boolean,如果包含返回true,不包含则返回false。
实际上,String的contains方法是通过调用indexOf方法来判断的,源码如下:
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
//包含Java
assertTrue("code in Java".contains("Java"));
//大小写敏感,不包含GO
assertFalse("Let's go".contains("GO"));
//转为大写后包含
assertTrue("Let's go".toUpperCase().contains("GO"));
assertTrue("code in Java".contains("Java"));
//大小写敏感,不包含GO
assertFalse("Let's go".contains("GO"));
//转为大写后包含
assertTrue("Let's go".toUpperCase().contains("GO"));
JDK原生 正则匹配Pattern
通过强大的正则匹配来判断,虽然有点杀鸡用牛刀的感觉,但也不是不能用,
Pattern pattern = Pattern.compile("Java");
//包含Java
Matcher matcher1 = pattern.matcher("Python, Java, Go, C++");
assertTrue(matcher1.find());
//不包含Java
Matcher matcher2 = pattern.matcher("Python, C, Go, Matlab");
assertFalse(matcher2.find());
//包含Java
Matcher matcher1 = pattern.matcher("Python, Java, Go, C++");
assertTrue(matcher1.find());
//不包含Java
Matcher matcher2 = pattern.matcher("Python, C, Go, Matlab");
assertFalse(matcher2.find());
Apache库StringUtils.contains
Apache的commons-lang3提供许多开箱即用的功能,StringUtils就提供了许多与字符串相关的功能
//包含sub
assertTrue(StringUtils.contains("String subString", "sub"));
//大小写敏感
assertFalse(StringUtils.contains("This is Java", "java"));
//忽略大小写
assertTrue(StringUtils.containsIgnoreCase("This is Java", "java"));
assertTrue(StringUtils.contains("String subString", "sub"));
//大小写敏感
assertFalse(StringUtils.contains("This is Java", "java"));
//忽略大小写
assertTrue(StringUtils.containsIgnoreCase("This is Java", "java"));
性能比较结果与使用建议
性能最好的是String的indexOf方法和contains方法,建议使用contains方法,性能好,跟indexOf相比,更直观,更不容易犯错。
毕竟让每个人时刻记住返回-1代表不存在也不是一件容易的事。
毕竟让每个人时刻记住返回-1代表不存在也不是一件容易的事。
背景
判断一个字符串是否包含某个特定子串是常见的场景,比如判断一篇文章是否包含敏感词汇、判断日志是否有ERROR信息等。
本文将介绍四种方法并进行性能测试。
四种方法
JDK原生方法String.indexOf
在String的函数中,提供了indexOf(subStr)方法,返回子串subStr第一次出现的位置,如果不存在则返回-1。
例子如下:
//包含Java
assertEquals(7, "Pkslow Java".indexOf("Java"));
//如果包含多个,返回第一次出现位置
assertEquals(0, "Java Java".indexOf("Java"));
//大小写敏感
assertEquals(-1, "Google Guava".indexOf("guava"));
assertEquals(7, "Pkslow Java".indexOf("Java"));
//如果包含多个,返回第一次出现位置
assertEquals(0, "Java Java".indexOf("Java"));
//大小写敏感
assertEquals(-1, "Google Guava".indexOf("guava"));
JDK原生方法String.contains
最直观判断的方法是contains(subStr),返回类型为boolean,如果包含返回true,不包含则返回false。
例子如下
//包含Java
assertTrue("code in Java".contains("Java"));
//大小写敏感,不包含GO
assertFalse("Let's go".contains("GO"));
//转为大写后包含
assertTrue("Let's go".toUpperCase().contains("GO"));
assertTrue("code in Java".contains("Java"));
//大小写敏感,不包含GO
assertFalse("Let's go".contains("GO"));
//转为大写后包含
assertTrue("Let's go".toUpperCase().contains("GO"));
实际上,String的contains方法是通过调用indexOf方法来判断的
源码
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
return indexOf(s.toString()) > -1;
}
JDK原生正则匹配Pattern
通过强大的正则匹配来判断,虽然有点杀鸡用牛刀的感觉,但也不是不能用
例子如下
Pattern pattern = Pattern.compile("Java");
//包含Java
Matcher matcher1 = pattern.matcher("Python, Java, Go, C++");
assertTrue(matcher1.find());
//不包含Java
Matcher matcher2 = pattern.matcher("Python, C, Go, Matlab");
assertFalse(matcher2.find());
//包含Java
Matcher matcher1 = pattern.matcher("Python, Java, Go, C++");
assertTrue(matcher1.find());
//不包含Java
Matcher matcher2 = pattern.matcher("Python, C, Go, Matlab");
assertFalse(matcher2.find());
Apache库StringUtils.contains
Apache的commons-lang3提供许多开箱即用的功能,StringUtils就提供了许多与字符串相关的功能
例子如下
//包含sub
assertTrue(StringUtils.contains("String subString", "sub"));
//大小写敏感
assertFalse(StringUtils.contains("This is Java", "java"));
//忽略大小写
assertTrue(StringUtils.containsIgnoreCase("This is Java", "java"));
assertTrue(StringUtils.contains("String subString", "sub"));
//大小写敏感
assertFalse(StringUtils.contains("This is Java", "java"));
//忽略大小写
assertTrue(StringUtils.containsIgnoreCase("This is Java", "java"));
性能对比
使用JMH工具来对四种方法进行性能测试
Maven引入代码如
org.openjdk.jmh
jmh-core
${openjdk.jmh.version}
org.openjdk.jmh
jmh-generator-annprocess
${openjdk.jmh.version}
jmh-core
${openjdk.jmh.version}
org.openjdk.jmh
jmh-generator-annprocess
${openjdk.jmh.version}
测试代码如下
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringContainsPerformanceTest {
@State(Scope.Thread)
public static class MyState {
private String text = "If you want to be smart; read. If you want to be really smart; read a lot.";
Pattern pattern = Pattern.compile("read");
}
@Benchmark
public int indexOf(MyState state) {
return state.text.indexOf("read");
}
@Benchmark
public boolean contains(MyState state) {
return state.text.contains("read");
}
@Benchmark
public boolean stringUtils(MyState state) {
return StringUtils.contains(state.text, "read");
}
@Benchmark
public boolean pattern(MyState state) {
return state.pattern.matcher(state.text).find();
}
public static void main(String[] args) throws Exception {
Options options = new OptionsBuilder()
.include(StringContainsPerformanceTest.class.getSimpleName())
.threads(6)
.forks(1)
.warmupIterations(3)
.measurementIterations(6)
.shouldFailOnError(true)
.shouldDoGC(true)
.build();
new Runner(options).run();
}
}
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringContainsPerformanceTest {
@State(Scope.Thread)
public static class MyState {
private String text = "If you want to be smart; read. If you want to be really smart; read a lot.";
Pattern pattern = Pattern.compile("read");
}
@Benchmark
public int indexOf(MyState state) {
return state.text.indexOf("read");
}
@Benchmark
public boolean contains(MyState state) {
return state.text.contains("read");
}
@Benchmark
public boolean stringUtils(MyState state) {
return StringUtils.contains(state.text, "read");
}
@Benchmark
public boolean pattern(MyState state) {
return state.pattern.matcher(state.text).find();
}
public static void main(String[] args) throws Exception {
Options options = new OptionsBuilder()
.include(StringContainsPerformanceTest.class.getSimpleName())
.threads(6)
.forks(1)
.warmupIterations(3)
.measurementIterations(6)
.shouldFailOnError(true)
.shouldDoGC(true)
.build();
new Runner(options).run();
}
}
测试结果如下
Benchmark Mode Cnt Score Error Units
contains avgt 6 11.331 ± 1.435 ns/op
indexOf avgt 6 11.250 ± 1.822 ns/op
pattern avgt 6 101.196 ± 12.047 ns/op
stringUtils avgt 6 29.046 ± 3.873 ns/op
contains avgt 6 11.331 ± 1.435 ns/op
indexOf avgt 6 11.250 ± 1.822 ns/op
pattern avgt 6 101.196 ± 12.047 ns/op
stringUtils avgt 6 29.046 ± 3.873 ns/op
最快的就是indexOf方法,其次是contains方法,二者应该没有实际区别,contains是调用indexOf来实现的。。
Apache的StringUtils为第三方库,相对慢一些。
最慢的是使用了正则的Pattern的方法,这不难理解,正则引擎的匹配是比较耗性能的
总结
本文介绍了判断一个字符串是否包含某个特定子串的四种方法,并通过性能测试进行了对比。
其中性能最好的是String的indexOf方法和contains方法,建议使用contains方法,性能好,跟indexOf相比,更直观,更不容易犯错。
毕竟让每个人时刻记住返回-1代表不存在也不是一件容易的事。
但是,使用indexOf和contains方法都需要注意做判空处理,这时StringUtils的优势就体现出来了。
对象的字符串表示
Object类有一个public String toString()方法,一个对象通过调用该方法可以获得该对象的字符串表示。
一个对象调用toString()方法返回的字符串的一般形式为:创建对象的类的名字@对象的引用的字符串表示
String 类的构造方法 与String创建的方法
String(char[])
用字符数组中的全部字符创建字符串对象
用指定的字节数组构造一个字符串对象
String(char[],int offset,int length)
用字符数组中的部分字符创建字符串对象
用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。
String可以通过许多途径创建, 也可以根据Stringbuffer和StringBuilder进行创建。
String创建的方法
String创建的方法
String类源码解读,String对象底层
用于存储字符的char数组,value[]
字符串是什么, 一连串字符组成的串。
一连串字符组成的串
通过char数组,来保存字符串的。
用于存储字符的char数组,value[]
这个数组存储了每个字符。
用于缓存字符串的哈希码,hash属性
因为String经常被用于比较, 比如在HashMap中。
如果每次进行比较,都重新计算其hashcode的值, 那无疑是比较麻烦的
而保存一个hashcode的缓存,无疑能优化这样的操作。
使用了StringBuilder对象的append方法,进行字符串拼接的
在继承树的什么位置、实现了什么接口、父类是谁
String没有继承任何接口, 不过实现了三个接口
Serializable接口
这个序列化接口没有任何方法和域, 仅用于标识序列化的语意。
Comparable接口
实现了Comparable的接口可用于内部比较两个对象的大小
CharSequence接口
字符串序列接口
CharSequence是一个可读的char值序列
提供了几个对字符序列进行只读访问的方法
length()
charAt(int index)
subSequence(int start,int end)
toString方法
StringBuilder和StringBuffer也继承了这个接口
从设计角度理解String的intern()方法
由于String的不可变性, 不可变对象在拷贝时,不需要额外的复制数据
String在JDK 1.6之后,提供了intern()方法
是什么?
intern()方法是一个native方法
它底层由C/C++实现
目的
把字符串缓存起来
JVM的不同存储区域(JVM的内存模型)
JVM的不同存储区域
方法区
各个线程共享的内存区域
用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
static修饰的变量存储在方法区中
运行时常量池
又被称为Runtime Constant Pool
这块区域是方法区的一部分
它的名字非常有意思,它并不要求常量一定只有在编译期才能产生
并非编译期间将常量放在常量池中, 运行期间也可以将新的常量放入常量池中,
String的intern方法就是一个典型的例子。
堆
堆是线程共享的数据区
堆是JVM中最大的一块存储区域
所有的对象实例, 包括实例变量都在堆上进行相应的分配。
虚拟机栈
线程私有的数据区
Java虚拟机栈的生命周期与线程相同
虚拟机栈也是局部变量的存储位置。
方法在执行过程中, 会在虚拟机栈种创建一个栈帧(stack frame) 。
程序计数器
线程私有的数据区
这部分区域用于存储线程的指令地址
用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成。
本地方法栈
线程私有的数据区
本地方法栈存储的区域主要是Java中使用native关键字修饰的方法所存储的区域
为什么在JDK 1.6中,不推荐使用intern()方法
在JDK 1.6及之前的版本中,
把方法区放到了永久代(Java堆的一部分)
永久代的空间是有限的
除了Full GC外, 其他收集并不会释放永久代的存储空间,
常量池是分配在方法区中永久代(Parmanent Generation) 内的
而永久代和Java堆是两个完全分开的区域中, 并且返回此String对象的引用。
如果字符串常量池中已经包含一个等于此String对象的字符串, 则返回常量池中这个字符串的String对象
否则, 将此String对象包含的字符串添加到常量池
一些人把方法区称为永久代, 这种说法不准确, 仅仅是Hotspot虚拟机设计团队选择使用永久代来实现方法区而已。
从JDK 1.7开始
将字符串常量池移到了堆内存中
去永久代, 字符串常量池已经被转移至Java堆中,开发人员也对intern方法做了一些修改。
因为字符串常量池和new的对象都存于Java堆中, 为了优化性能和减少内存开销, 当调用intern方法时,
如果常量池中已经存在该字符串, 则返回池中字符串:
否则,直接存储堆中的引用
字符串常量池中存储的是指向堆里的对象。
intern方法的使用代码
认识一下intern方法
上述的执行结果是什么呢?
false true true false
和你预想的一样吗?为什么会这样呢?我们先来看一下intem方法的官方解释
intem方法的官方解释
第一个
输出什么?false, 为什么呢?
a.intern返回的是常量池中的ab, 而b是直接返回的是堆中的ab.地址不一样, 肯定输出false
图解
图画的有些问题, 栈应该是后入先出, 所以b应该在a上面,不过不影响效果
第二个
都返回的是字符串常量池中的ab, 地址相同,所以输出true |
图解
图解
第三个
a不会变,因为常量池中已经有了ab,
所以c不会再创建一个ab字符串,这是编译器做的优化,为了提高效率。
图解
图解
第四个
子主题
图解
图解
认识一下intern方法
String 对“+”的重载
1、String s = "a" + "b",编译器会进行常量折叠,即变成String s = "ab"
常量折叠(因为两个都是编译期常量,编译期可知)
2、对于能够进行优化的(String s = "a" + 变量等)用StringBuilder 的append()
方法替代,最后调用toString() 方法(底层就是一个new String())
方法替代,最后调用toString() 方法(底层就是一个new String())
Java 7 中switch对String的支持
Java 7 中,switch 的参数可以是String 类型了,这对我们来说是一个很方便的改进。
到目前为止switch 支持这样几种数据类型:byte 、short、int 、char 、String 。
switch 对整型的支持是怎么实现的呢?对字符型是怎么实现的呢?String 类型呢?
switch 到底是如何实现的?底层实现原理
疑问抛出
switch 对整型的支持是怎么实现的呢?
对字符型是怎么实现的呢?
String 类型呢?
猜测
switch 对String 的支持是使用
equals()方法
hashcode()方法
switch 对整型支持的实现
定义一个int 型变量a,然后使用switch 语句进行判断。执行这段代码输出内容为5,
样例代码
样例代码
对代码进行反编译
反编译后的代码
反编译后的代码和之前的代码比较,除了多了两行注释以外没有任何区别,
switch 对整型int的判断是直接比较整数的值。
switch 对字符型支持的实现
样例代码
直接上代码
对代码进行反编译
编译后的代码
通过以上的代码作比较发现:
对char 类型进行比较的时候,实际上比较的是ascii 码,
编译器会把char 型变量转换成对应的int 型变量。
switch 对字符串支持的实现
样例代码
样例代码
对代码进行反编译
对代码进行反编译
字符串的switch 是通过equals()和hashCode()方法来实现的。
记住,switch 中只能使用整型,比如byte,short,char(ackii 码是整型)以及int。
还好hashCode()方法返回的是int,而不是long。通过这个很容易记住hashCode返回的是int 这个事实。
仔细看下可以发现,进行switch 的实际是哈希值,然后通过使用equals 方法比较进行安全检查,
这个检查是必要的,因为哈希可能会发生碰撞。
因此它的性能是不如使用枚举进行switch 或者使用纯整数常量,但这也不是很差。
因为Java 编译器只增加了一个equals 方法,如果你比较的是字符串字面量的话会非常快,
比如”abc”==”abc”。
如果你把hashCode()方法的调用也考虑进来了,那么还会再多一次的调用开销,
因为字符串一旦创建了,它就会把哈希值缓存起来。
因此如果这个switch 语句是用在一个循环里的,比如逐项处理某个值,或者游戏引擎循环地渲染屏幕,
这里hashCode()方法的调用开销其实不会很大。
总结:其实switch 只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后在使用switch 的。
(常见)字符串乱码问题
java字符串的各种编码转换
参考代码
import java.io.UnsupportedEncodingException;
/**
* 转换字符串的编码
*/
public class ChangeCharset {
/** 7位ASCII字符,也叫作ISO646-US、Unicode字符集的基本拉丁块 */
public static final String US_ASCII = "US-ASCII";
/** ISO 拉丁字母表 No.1,也叫作 ISO-LATIN-1 */
public static final String ISO_8859_1 = "ISO-8859-1";
/** 8 位 UCS 转换格式 */
public static final String UTF_8 = "UTF-8";
/** 16 位 UCS 转换格式,Big Endian(最低地址存放高位字节)字节顺序 */
public static final String UTF_16BE = "UTF-16BE";
/** 16 位 UCS 转换格式,Little-endian(最高地址存放低位字节)字节顺序 */
public static final String UTF_16LE = "UTF-16LE";
/** 16 位 UCS 转换格式,字节顺序由可选的字节顺序标记来标识 */
public static final String UTF_16 = "UTF-16";
/** 中文超大字符集 */
public static final String GBK = "GBK";
/**
* 将字符编码转换成US-ASCII码
*/
public String toASCII(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, US_ASCII);
}
/**
* 将字符编码转换成ISO-8859-1码
*/
public String toISO_8859_1(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, ISO_8859_1);
}
/**
* 将字符编码转换成UTF-8码
*/
public String toUTF_8(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, UTF_8);
}
/**
* 将字符编码转换成UTF-16BE码
*/
public String toUTF_16BE(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, UTF_16BE);
}
/**
* 将字符编码转换成UTF-16LE码
*/
public String toUTF_16LE(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, UTF_16LE);
}
/**
* 将字符编码转换成UTF-16码
*/
public String toUTF_16(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, UTF_16);
}
/**
* 将字符编码转换成GBK码
*/
public String toGBK(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, GBK);
}
/**
* 字符串编码转换的实现方法
* @param str 待转换编码的字符串
* @param newCharset 目标编码
* @return
* @throws UnsupportedEncodingException
*/
public String changeCharset(String str, String newCharset)
throws UnsupportedEncodingException {
if (str != null) {
//用默认字符编码解码字符串。
byte[] bs = str.getBytes();
//用新的字符编码生成字符串
return new String(bs, newCharset);
}
return null;
}
/**
* 字符串编码转换的实现方法
* @param str 待转换编码的字符串
* @param oldCharset 原编码
* @param newCharset 目标编码
* @return
* @throws UnsupportedEncodingException
*/
public String changeCharset(String str, String oldCharset, String newCharset)
throws UnsupportedEncodingException {
if (str != null) {
//用旧的字符编码解码字符串。解码可能会出现异常。
byte[] bs = str.getBytes(oldCharset);
//用新的字符编码生成字符串
return new String(bs, newCharset);
}
return null;
}
public static void main(String[] args) throws UnsupportedEncodingException {
ChangeCharset test = new ChangeCharset();
String str = "This is a 中文的 String!";
System.out.println("str: " + str);
String gbk = test.toGBK(str);
System.out.println("转换成GBK码: " + gbk);
System.out.println();
String ascii = test.toASCII(str);
System.out.println("转换成US-ASCII码: " + ascii);
gbk = test.changeCharset(ascii,ChangeCharset.US_ASCII, ChangeCharset.GBK);
System.out.println("再把ASCII码的字符串转换成GBK码: " + gbk);
System.out.println();
String iso88591 = test.toISO_8859_1(str);
System.out.println("转换成ISO-8859-1码: " + iso88591);
gbk = test.changeCharset(iso88591,ChangeCharset.ISO_8859_1, ChangeCharset.GBK);
System.out.println("再把ISO-8859-1码的字符串转换成GBK码: " + gbk);
System.out.println();
String utf8 = test.toUTF_8(str);
System.out.println("转换成UTF-8码: " + utf8);
gbk = test.changeCharset(utf8,ChangeCharset.UTF_8, ChangeCharset.GBK);
System.out.println("再把UTF-8码的字符串转换成GBK码: " + gbk);
System.out.println();
String utf16be = test.toUTF_16BE(str);
System.out.println("转换成UTF-16BE码:" + utf16be);
gbk = test.changeCharset(utf16be,ChangeCharset.UTF_16BE, ChangeCharset.GBK);
System.out.println("再把UTF-16BE码的字符串转换成GBK码: " + gbk);
System.out.println();
String utf16le = test.toUTF_16LE(str);
System.out.println("转换成UTF-16LE码:" + utf16le);
gbk = test.changeCharset(utf16le,ChangeCharset.UTF_16LE, ChangeCharset.GBK);
System.out.println("再把UTF-16LE码的字符串转换成GBK码: " + gbk);
System.out.println();
String utf16 = test.toUTF_16(str);
System.out.println("转换成UTF-16码:" + utf16);
gbk = test.changeCharset(utf16,ChangeCharset.UTF_16LE, ChangeCharset.GBK);
System.out.println("再把UTF-16码的字符串转换成GBK码: " + gbk);
String s = new String("中文".getBytes("UTF-8"),"UTF-8");
System.out.println(s);
}
}
/**
* 转换字符串的编码
*/
public class ChangeCharset {
/** 7位ASCII字符,也叫作ISO646-US、Unicode字符集的基本拉丁块 */
public static final String US_ASCII = "US-ASCII";
/** ISO 拉丁字母表 No.1,也叫作 ISO-LATIN-1 */
public static final String ISO_8859_1 = "ISO-8859-1";
/** 8 位 UCS 转换格式 */
public static final String UTF_8 = "UTF-8";
/** 16 位 UCS 转换格式,Big Endian(最低地址存放高位字节)字节顺序 */
public static final String UTF_16BE = "UTF-16BE";
/** 16 位 UCS 转换格式,Little-endian(最高地址存放低位字节)字节顺序 */
public static final String UTF_16LE = "UTF-16LE";
/** 16 位 UCS 转换格式,字节顺序由可选的字节顺序标记来标识 */
public static final String UTF_16 = "UTF-16";
/** 中文超大字符集 */
public static final String GBK = "GBK";
/**
* 将字符编码转换成US-ASCII码
*/
public String toASCII(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, US_ASCII);
}
/**
* 将字符编码转换成ISO-8859-1码
*/
public String toISO_8859_1(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, ISO_8859_1);
}
/**
* 将字符编码转换成UTF-8码
*/
public String toUTF_8(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, UTF_8);
}
/**
* 将字符编码转换成UTF-16BE码
*/
public String toUTF_16BE(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, UTF_16BE);
}
/**
* 将字符编码转换成UTF-16LE码
*/
public String toUTF_16LE(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, UTF_16LE);
}
/**
* 将字符编码转换成UTF-16码
*/
public String toUTF_16(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, UTF_16);
}
/**
* 将字符编码转换成GBK码
*/
public String toGBK(String str) throws UnsupportedEncodingException{
return this.changeCharset(str, GBK);
}
/**
* 字符串编码转换的实现方法
* @param str 待转换编码的字符串
* @param newCharset 目标编码
* @return
* @throws UnsupportedEncodingException
*/
public String changeCharset(String str, String newCharset)
throws UnsupportedEncodingException {
if (str != null) {
//用默认字符编码解码字符串。
byte[] bs = str.getBytes();
//用新的字符编码生成字符串
return new String(bs, newCharset);
}
return null;
}
/**
* 字符串编码转换的实现方法
* @param str 待转换编码的字符串
* @param oldCharset 原编码
* @param newCharset 目标编码
* @return
* @throws UnsupportedEncodingException
*/
public String changeCharset(String str, String oldCharset, String newCharset)
throws UnsupportedEncodingException {
if (str != null) {
//用旧的字符编码解码字符串。解码可能会出现异常。
byte[] bs = str.getBytes(oldCharset);
//用新的字符编码生成字符串
return new String(bs, newCharset);
}
return null;
}
public static void main(String[] args) throws UnsupportedEncodingException {
ChangeCharset test = new ChangeCharset();
String str = "This is a 中文的 String!";
System.out.println("str: " + str);
String gbk = test.toGBK(str);
System.out.println("转换成GBK码: " + gbk);
System.out.println();
String ascii = test.toASCII(str);
System.out.println("转换成US-ASCII码: " + ascii);
gbk = test.changeCharset(ascii,ChangeCharset.US_ASCII, ChangeCharset.GBK);
System.out.println("再把ASCII码的字符串转换成GBK码: " + gbk);
System.out.println();
String iso88591 = test.toISO_8859_1(str);
System.out.println("转换成ISO-8859-1码: " + iso88591);
gbk = test.changeCharset(iso88591,ChangeCharset.ISO_8859_1, ChangeCharset.GBK);
System.out.println("再把ISO-8859-1码的字符串转换成GBK码: " + gbk);
System.out.println();
String utf8 = test.toUTF_8(str);
System.out.println("转换成UTF-8码: " + utf8);
gbk = test.changeCharset(utf8,ChangeCharset.UTF_8, ChangeCharset.GBK);
System.out.println("再把UTF-8码的字符串转换成GBK码: " + gbk);
System.out.println();
String utf16be = test.toUTF_16BE(str);
System.out.println("转换成UTF-16BE码:" + utf16be);
gbk = test.changeCharset(utf16be,ChangeCharset.UTF_16BE, ChangeCharset.GBK);
System.out.println("再把UTF-16BE码的字符串转换成GBK码: " + gbk);
System.out.println();
String utf16le = test.toUTF_16LE(str);
System.out.println("转换成UTF-16LE码:" + utf16le);
gbk = test.changeCharset(utf16le,ChangeCharset.UTF_16LE, ChangeCharset.GBK);
System.out.println("再把UTF-16LE码的字符串转换成GBK码: " + gbk);
System.out.println();
String utf16 = test.toUTF_16(str);
System.out.println("转换成UTF-16码:" + utf16);
gbk = test.changeCharset(utf16,ChangeCharset.UTF_16LE, ChangeCharset.GBK);
System.out.println("再把UTF-16码的字符串转换成GBK码: " + gbk);
String s = new String("中文".getBytes("UTF-8"),"UTF-8");
System.out.println(s);
}
}
使用String(byte[] bytes, String encoding)构造字符串时
Java中的String类是按照unicode进行编码的
encoding所指的是bytes中的数据是按照那种方式编码的
而不是最后产生的String是什么编码方式
让系统把bytes中的数据由encoding编码方式转换成unicode编码。
如果不指明,bytes的编码方式将由jdk根据操作系统决定
使用String构造函数的时候注意事项
当从文件中读数据时,最好使用InputStream方式,然后采用String(byte[] bytes, String encoding)指明文件的编码方式。
不要使用Reader方式,因为Reader方式会自动根据jdk指明的编码方式把文件内容转换成unicode 编码。
从HttpRequest中读参数时,利用reqeust.setCharacterEncoding()方法设置编码方式,读出的内容就是正确的了。
当从数据库中读文本数据时,采用ResultSet.getBytes()方法取得字节数组,同样采用带编码方式的字符串构造方法即可。
(正确的使用方式)ResultSet.getBytes()方法演示
ResultSet rs;
bytep[] bytes = rs.getBytes();
String str = new String(bytes, "gb2312");
bytep[] bytes = rs.getBytes();
String str = new String(bytes, "gb2312");
(错误的使用方式)ResultSet.getBytes()方法演示
ResultSet rs;
String str = rs.getString();
str = new String(str.getBytes("iso8859-1"), "gb2312");
String str = rs.getString();
str = new String(str.getBytes("iso8859-1"), "gb2312");
这种编码转换方式效率底。
之所以这么做的原因是,ResultSet在getString()方法执行时,默认数据库里的数据编码方式为 iso8859-1。
系统会把数据依照iso8859-1的编码方式转换成unicode。
使用str.getBytes("iso8859-1")把数 据还原,然后利用new String(bytes, "gb2312")把数据从gb2312转换成unicode,中间多了好多步骤。
三种常量池
字符串常量池
字符串常量池
全局字符串常量池
英文
string pool
(也有叫做string literal pool)
保存在JVM内存的方法区中
在JVM 中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。
会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池。
字符串常量池中存的是引用值,而不是具体的实例对象
具体的实例对象是在堆中开辟的一块空间存放的
在每个VM中只有一份
全局常量池在每个VM中只有一份,存放的是key(字面量“abc”)-value(字符串"abc"实例对象在堆中的引用)键值对。
全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中
HotSpotVM中的字符串常量的实现原理(享元模式)
在HotSpotVM里实现的string pool功能的是一个StringTable类
它是一个哈希表,里面存的是key(字面量“abc”, 即驻留字符串)-value(字符串"abc"实例对象在堆中的引用)键值对
也就是说在堆中的某些字符串实例被这个StringTable引用之后,就等同被赋予了”驻留字符串”的身份。
这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享(享元模式)
字符串驻留或池化机制
当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查
如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回
否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。
intern
除了以上方式(字符串驻留或池化机制)之外,还有一种可以在运行期,将字符串内容放置到字符串常量池的办法,那就是使用intern。
在每次赋值时,使用String 的intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用。
字符串常量池的位置
JDK 7之前
放在永久代中的。
JDK7
因为按照计划,JDK 会在后续的版本中通过元空间来代替永久代
所以在JDK7 中,将字符串常量池先从永久代中移出,暂时放到了堆内存中。
JDK 8
彻底移除了永久代,使用元空间替代了永久代,
于是字符串常量池再次从堆内存移动到永久代中。
Class 常量池
什么是Class 文件 ?
从编译说起
计算机只认识0 和1,所以程序员写的代码都需要经过编译成0 和1 构成的二进制格式才能够让计算机运行。
如Groovy、JRuby、Jython、Scala 等
字节码(ByteCode)
为了让Java 语言具有良好的跨平台能力,Java 独具匠心的提供了一种可以在所有平台上都能使用的一种中间代码——字节码(ByteCode)。
有了字节码,无论是哪种平台(如Windows、Linux 等),只要安装了虚拟机,都可以直接运行字节码。
字节码解除了Java 虚拟机和Java 语言间的耦合
同样,有了字节码,也解除了Java 虚拟机和Java 语言之间的耦合。
这话可能很多人不理解,Java 虚拟机不就是运行Java 语言的么?这种解耦指的是什么?
其实,目前Java 虚拟机已经可以支持很多除Java 语言以外的语言了。
之所以可以支持,就是因为这些语言也可以被编译成字节码。而虚拟机并不关心字节码是有哪种语言编译而来的。
当java文件被编译成class文件之后,就是会生成class常量池
如何查看常量池 ?
通过javac 命令,生成class 文件
Java 语言中负责编译出字节码的编译器是一个命令是javac。
javac 是收录于JDK 中的Java 语言编译器。
该工具可以将后缀名为.java 的源文件编译为后缀名为.class 的可以运行于Java 虚拟机的字节码。
javac命令的使用
如,我们有以下简单的HelloWorld.java 代码:
public class HelloWorld {
public static void main(String[] args) {
String s = "Hollis";
}}
public static void main(String[] args) {
String s = "Hollis";
}}
通过javac 命令生成class 文件
javac HelloWorld.java
生成HelloWorld.class 文件:
生成HelloWorld.class 文件:
如何使用16 进制打开class 文件:使用vim test.class ,然后在交互模式下,输入:%!xxd 即可。
可以看到,上面的文件就是Class 文件,
要想能够读懂上面的字节码,需要了解Class 类文件的结构
解读HelloWorld.class 文件(版本号后面的就是Class 常量池入口)
生成HelloWorld.class 文件:
HelloWorld.class 文件中的前八个字母是cafe babe,这就是Class 文件的魔数(Java 中的”魔数”)。
在Class 文件的4 个字节的魔数后面的分别是
4 个字节的Class文件的版本号
第5、6 个字节是次版本号
第7、8 个字节是主版本号
生成的Class文件的版本号是52,这时Java 8 对应的版本。
也就是说,这个版本的字节码,在JDK1.8 以下的版本中无法运行
在版本号后面的,就是Class 常量池入口了。
通过javap 命令,查看Class 文件中常量池
javap -v HelloWorld.class
从上图中可以看到,反编译后的class 文件常量池中共有16 个常量。
反编译后的class 文件常量池中共有16 个常量
而Class 文件中常量计数器的数值是0011,将该16 进制数字转换成10 进制的结果是17。
Class 文件中常量计数器的数值是0011
原因是与Java 的语言习惯不同,
常量池计数器是从0 开始而不是从1 开始的,常量池的个数是十进制的17,
这就代表了其中有16 个常量,索引值范围为1-16。
Class 常量池可以理解为是Class 文件中的资源仓库。
Class 常量池是Class 文件中的资源仓库,其中保存了各种常量。而这些常量都是开发者定义出来,需要在程序的运行期使用的。
Class 文件中包含
Java 虚拟机指令集
符号表
若干其他辅助信息
类的版本、字段、方法、接口等描述信息
常量池(constant pool table)
常量池(constant pool table)
它是Class文件中的内容,还不是运行时的内容,
不要理解它是个池子,其实就是Class文件中的字节码指令
class常量池是class字节码文件里的内容,在编译阶段,存放的是与类相关的常量。
用于存放编译器生成的各种字面量 + 符号引用
各种字面量(Literal)
计算机科学中关于字面量的解释
在计算机科学中,字面量(Literal)是用于表达源代码中一个固定值的表示法(notation)。
几乎所有计算机编程语言都具有对基本值的字面量表示,
诸如:整数、浮点数以及字符串
而有很多也对布尔类型和字符类型的值也支持字面量表示
还有一些甚至对复合类型的值也支持字面量表示法。
枚举类型的元素
像数组、记录和对象等
字面量的定义
字面量就是指由字母、数字等构成的字符串或者数值。
字面量就是常量,如文本字符串、被声明为final的常量值等。
字面量只可以右值出现
所谓右值是指等号右边的值
如:int a=123 ,这里的a 为左值,123 为右值。
在这个例子中123 就是字面量。
int a = 123;
String s = "hollis";
String s = "hollis";
本文开头的HelloWorld 代码中,Hollis 就是一个字面量。
符号引用(Symbolic References)
什么是符号引用
编译原理中的概念
是相对于直接引用来说的。
一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)
一般包括下面三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
字段的名称和描述符
方法的名称和描述符
主要包括了以下三类常量
* 类和接口的全限定名
* 字段的名称和描述符
* 方法的名称和描述符
这也就可以印证前面的常量池中还包含一些com/hollis/HelloWorld、main、([Ljava/lang/String;)V 等常量的原因了。
常量池容量计数器
由于不同的Class 文件中包含的常量的个数是不固定的
所以在Class 文件的常量池入口处会设置两个字节的常量池容量计数器
记录了常量池中常量的个数
解读文件(版本号后面的就是Class 常量池入口)
常量池的每一项常量都是一个表
一共有如下表所示的11种各不相同的表结构数据
这每个表开始的第一位都是一个字节的标志位(取值1-12)
代表当前这个常量属于哪种常量类型。 每种不同类型的常量类型具有不同的结构
Class 常量池有什么用
在《深入理解Java 虚拟》中有这样的表述:
Java 代码在进行Javac 编译的时候,并不像C 和C++那样有“连接”这一步骤,而
是在虚拟机加载Class 文件的时候进行动态连接。也就是说,在Class 文件中不会保存各
个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话
无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量
池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类
的创建和动态连接的内容,在虚拟机类加载过程时再进行详细讲解。
前面这段话,看起来很绕,不是很容易理解。其实他的意思就是: Class 是用来保存
常量的一个媒介场所,并且是一个中间场所。在JVM 真的运行时,需要把常量池中的常量
加载到内存中。
至于到底哪个阶段会做这件事情,以及Class 常量池中的常量会以何种方式被加载到
具体什么地方, 会在本系列文章的后续内容中继续阐述。
是在虚拟机加载Class 文件的时候进行动态连接。也就是说,在Class 文件中不会保存各
个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话
无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量
池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类
的创建和动态连接的内容,在虚拟机类加载过程时再进行详细讲解。
前面这段话,看起来很绕,不是很容易理解。其实他的意思就是: Class 是用来保存
常量的一个媒介场所,并且是一个中间场所。在JVM 真的运行时,需要把常量池中的常量
加载到内存中。
至于到底哪个阶段会做这件事情,以及Class 常量池中的常量会以何种方式被加载到
具体什么地方, 会在本系列文章的后续内容中继续阐述。
Class 是用来保存常量的一个媒介场所,并且是一个中间场所。
在JVM 真的运行时,需要把常量池中的常量加载到内存中。
运行时常量池
定义
Runtime Constant Pool
运行时的内容,从常量池转化而来。
是每一个类或接口的常量池(Constant_Pool)的运行时表示形式。
将每个class常量池中的符号引用值转存到运行时常量池中,
也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后,才能获得的方法或字段引用。
扮演了类似传统语言中符号表( SymbolTable)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。
每一个运行时常量池都分配在Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
保存在JVM内存的方法区的中的,主要保存的是
字面量
符号引用
运行时常量池又是什么时候产生的呢?
jvm在加载某个类时,必须经过装载、连接、初始化,而连接又包括验证、准备、解析三个阶段。
而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,故运行时常量池也是每个类都有一个。
class常量池中存的是字面量和符号引用,也就是说Class常量池存的并不是对象的实例,而是对象的符号引用值。
而经过解析(resolve)之后,也就是把符号引用替换为直接引用,
解析的过程会去查询全局字符串池,也就是StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
运行时常量池在JDK 各个版本中的实现
根据Java 虚拟机规范约定:
每一个运行时常量池都在JVM方法区中分配,在加载类和接口到虚拟机后,就创建对应的运行时常量池。
在不同版本的JDK 中,运行时常量池所处的位置也不一样
以HotSpot 为例:
在JDK 1.7 之前
方法区位于堆内存的永久代中,运行时常量池作为方法区的一部分,也处于永久代中。
因为使用永久代实现方法区可能导致内存泄露问题,所以,
从JDK1.7 开始
JVM 尝试解决这一问题
在1.7 中,将原本位于永久代中的运行时常量池移动到堆内存中。
(永久代在JDK 1.7 并没有完全移除,只是原来方法区中的运行时常量池、类的静态变量等移动到了堆内存中。)
在JDK 1.8 中
彻底移除了永久代,方法区通过元空间的方式实现。
随之,运行时常量池也在元空间中实现
运行时常量池中常量的来源
运行时常量池中包含了若干种不同的常量:
编译期可知的字面量和符号引用(来自Class 常量池)。
运行期解析后可获得的常量(如String 的intern 方法)
运行时常量池中的内容包含
Class 常量池中的常量、
字符串常量池中的内容
运行时常量池、
Class 常量池、
字符串常量池的区别与联系
三个常量池之间的联系
虚拟机启动过程中,会将各个Class 文件中的常量池载入到运行时常量池中。
所以, Class 常量池只是一个媒介场所。
在JVM 真的运行时,需要把常量池中的常量加载到内存中,进入到运行时常量池。
字符串常量池可以理解为运行时常量池分出来的部分。
加载时,对于class 的静态常量池,如果字符串会被装到字符串常量池中。
String 有没有长度限制?
String 的长度限制
想要搞清楚这个问题,首先我们需要翻阅一下String 的源码,看下其中是否有关于长度的限制或者定义。
String 类中有很多重载的构造函数,其中有几个是支持用户传入length 来执行长度的:
public String(byte bytes[], int offset, int length)
参数length 是使用int 类型定义的,String 定义时,最大支持的长度就是int 的最大范围值。
根据Integer 类的定义,java.lang.Integer#MAX_VALUE 的最大值是2^31 - 1;
那么,是不是就可以认为String 能支持的最大长度就是这个值了呢?
其实并不是,这个值只是在运行期,构造String 时,可以支持的一个最大长度
实际上,在编译期,定义字符串的时候也是有长度限制的。
错误: 常量字符串过长
如以下代码:
String s = "11111...1111";//其中有10 万个字符"1"
当使用如上形式定义一个字符串时,执行javac 编译时,是会抛出异常的,提示如下:
明明String构造函数指定的长度是可以支持2147483647(2^31 - 1),为什么以上无法编译呢?
其实,形如String s = "xxx";定义String 时,xxx 称之为字面量,
这种字面量在编译之后,会以常量的形式进入到Class 常量池。
那么问题就来了,因为要进入常量池,就要遵守常量池的有关规定。
常量池限制
javac 是将Java 文件编译成class 文件的一个命令,那么在Class 文件生成过程中,就需要遵守一定的格式。
常量池的定义
根据《Java 虚拟机规范》中第4.4 章节常量池的定义,
CONSTANT_String_info结构
用于表示java.lang.String 类型的常量对象
格式如下:
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
u1 tag;
u2 string_index;
}
其中,string_index 项的值必须是对常量池的有效索引,
常量池在该索引处的项必须是CONSTANT_Utf8_info 结构,
表示一组Unicode 码点序列,这组Unicode 码点序列最终会被初始化为一个String 对象。
CONSTANT_Utf8_info 结构
用于表示字符串常量的值
格式如下
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
u1 tag;
u2 length;
u1 bytes[length];
}
其中,length 则指明了bytes[]数组的长度,其类型为u2。
通过翻阅《规范》,u2 表示两个字节的无符号数,那么1 个字节有8位,2 个字节就有16 位。
16 位无符号数可表示的最大值位2^16 - 1 = 65535。
Class 文件中常量池的格式规定了,其字符串常量的长度不能超过65535。
尝试使用以下方式定义字符串
String s = "11111...1111";//其中有65535 个字符"1"
尝试使用javac 编译,同样会得到"错误: 常量字符串过长",那么原因是什么呢?
其实,这个原因在javac 的代码中是可以找到的,
在Gen 类中有如下代码:
private void checkStringConstant(DiagnosticPosition var1, Object var2) {
if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)v
ar2).length() >= 65535) {
this.log.error(var1, "limit.string", new Object[0]);
++this.nerrs;
}
}
if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)v
ar2).length() >= 65535) {
this.log.error(var1, "limit.string", new Object[0]);
++this.nerrs;
}
}
代码中可以看出,当参数类型为String,并且长度大于等于65535时,就会导致编译失败。
这个地方可以尝试着debug 一下javac 的编译过程(,也可以发现这个地方会报错。
如果我们尝试以65534 个字符定义字符串,则会发现可以正常编译。
其实,关于这个值,在《Java 虚拟机规范》也有过说明:
if the Java Virtual Machine code for a method is exactly 65535 bytes
long and ends with an instruction that is 1 byte long, then that instruction
cannot be protected by an exception handler. A compiler writer can work
around this bug by limiting the maximum size of the generated Java Virtual
Machine code for any method, instance initialization method, or static
initializer (the size of any code array) to 65534 bytes.
long and ends with an instruction that is 1 byte long, then that instruction
cannot be protected by an exception handler. A compiler writer can work
around this bug by limiting the maximum size of the generated Java Virtual
Machine code for any method, instance initialization method, or static
initializer (the size of any code array) to 65534 bytes.
运行期限制
上面提到的这种String 长度的限制是编译期的限制,也就是使用String s= “”;这种字面值方式定义的时候才会有的限制。
那么,String 在运行期有没有限制呢
答案是有的,
就是前文提到的那个Integer.MAX_VALUE ,这个值约等于4G,
在运行期,如果String 的长度超过这个范围,就可能会抛出异常。
int 是一个32 位变量类型,取正数部分来算的话,他们最长可以有:
(在jdk 1.9 之前)
2^31-1 =2147483647 个16-bit Unicodecharacter
2147483647 * 16 = 34359738352 位
34359738352 / 8 = 4294967294 (Byte)
4294967294 / 1024 = 4194303.998046875 (KB)
4194303.998046875 / 1024 = 4095.9999980926513671875 (MB)
4095.9999980926513671875 / 1024 = 3.99999999813735485076904296875 (GB)
2147483647 * 16 = 34359738352 位
34359738352 / 8 = 4294967294 (Byte)
4294967294 / 1024 = 4194303.998046875 (KB)
4194303.998046875 / 1024 = 4095.9999980926513671875 (MB)
4095.9999980926513671875 / 1024 = 3.99999999813735485076904296875 (GB)
有近4G 的容量。
编译时最大长度都要求小于65535 了,运行期怎么会出现大于65535 的情况呢
这其实很常见,如以下代码:
String s = "";
for (int i = 0; i <100000 ; i++) {s+="i";}
for (int i = 0; i <100000 ; i++) {s+="i";}
得到的字符串长度就有10 万,另外我之前在实际应用中遇到过这个问题。
之前一次系统对接,需要传输高清图片,约定的传输方式是对方将图片转成BASE6编码,接收到之后再转成图片。
在将BASE64 编码后的内容,赋值给字符串时,就抛了异常。
总结
字符串有长度限制,在编译期,要求字符串常量池中的常量不能超过65535,并且在javac 执行过程中控制了最大值为65534。
在运行期,长度不能超过Int 的范围,否则会抛异常。
线程安全的StringBuffer对象
StringBuffer是什么?
用来定义字符串变量,它的对象是可以扩充和修改的。
StringBuffer对象代表一个可变的字符串序列
当一个StringBuffer被创建以后, 通过StringBuffer的一系列方法,可实现字符串拼接、截取等操作
一旦通过StringBuffer生成了最终想要的字符串后, 就可以调用其toString方法来生成一个新的字符串。
使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。
所以多数情况下推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。
StringBuffer是线程安全的,内部有大部分方法都加了synchronized锁
StringBuffer在字符串拼接上面,直接使用synchronized关键字加锁, 从而保证了线程安全性。
StringBuffer和StringBuilder最大的区别,谁是线程安全的
StringBuilder
StringBuilder不是线程安全的
StringBuffer
继承自AbstractStringBuilder
StringBuffer是线程安全的
可以在多线程场景下使用
StringBuffer比StringBuilder多了synchronized修饰符。
在单线程场景下,效率比较低, 因为有锁的开销。
线程安全的,为了解决String的操作产生很多中间String对象的问题。
同步方法是在对应的操作方法中简单粗暴的加上syncronized关键字。
StringBuffer内部有大部分方法都加了synchronized锁
看一下StringBuffer 的append 方法。
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
toStringCache = null;
super.append(str);
return this;
}
该方法使用synchronized 进行声明,说明是一个线程安全的方法。
什么是线程安全?
线程安全指多个线程执行同一段代码时采用加锁机制,使每次的执行结果和单线程执行结果一致,不存在执行时出意外。
不安全指不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得数据是脏数据。
StringBuffer类有三个构造方法
1.StringBuffer()
2.StringBuffer(int size)
3.StringBuffer(String s)
2.StringBuffer(int size)
3.StringBuffer(String s)
StringBuffer类的常用方法
StringBuffer append(String s)
将一个字符串对象追加到当前StringBuffer对象中
StringBuffer.append方法会自动调用toString方法吗?
上面提到+操作符连接两个字符串, 会自动执行toString方法
StringBuffer.append方法会自动调用toString方法吗?
上图左边是手动调用toString方法的代码, 右图是没有调用toString方法的代码
toString0方法不像+一样自动被调用。
StringBuffer append(int n)
将一个int型数据转化为字符串对象后,再追加到当前StringBuffer对象中
类似的方法还有
StringBuffer append(long n)
StringBuffer append(boolean n)
StringBuffer append(float n)
StringBuffer append(double n)
StringBuffer append(char n)
StringBuffer append(Object o)
将一个Object对象的字符串表示追加到当前StringBuffer对象中
public chat charAt(int n )
得到参数n指定的置上的单个字符
public void setCharAt(int n ,char ch)
将当前StringBuffer对象实体中的字符串位置n处的字符用参数ch指定的字符替换
StringBuffer insert(int index, String str)
将参数str指定的字符串插入到参数index指定的位置
public StringBuffer reverse()
将该对象实体中的字符翻转
StringBuffer replace( int startIndex ,int endIndex, String str)
将当前StringBuffer对象实体中的字符串的一个子字符串用参数str指定的字符串替换
StringBuffer delete(int startIndex, int endIndex)
从当前StringBuffer对象实体中的字符串中删除一个子字符串
其相关方法
deleteCharAt(int index)
删除当前StringBuffer对象实体的字符串中index位置处的一个字符。
StringBuffer的使用举例
StringBuffer对象的创建
String s = new String(“我喜欢散步");
StringBuffer buffer = new StringBuffer(“我喜欢”);
buffer.append("玩篮球 ");
StringBuffer buffer = new StringBuffer(“我喜欢”);
buffer.append("玩篮球 ");
实体不可变
实体可变
使用StringBuffer 可以方便的对字符串进行拼接
StringBuffer wechat = new StringBuffer("Hollis");
String introduce = "每日更新Java 相关技术文章";
StringBuffer hollis = wechat.append(",").append(introduce);
String introduce = "每日更新Java 相关技术文章";
StringBuffer hollis = wechat.append(",").append(introduce);
StringBuffer的使用
牛逼的StringBuilder对象
StringBuilder是什么?
StringBuilder是5.0新增的
StringBuilder类表示一个可变的字符序列
StringBuilder是一个牛逼的对象。
此类提供一个与 StringBuffer 兼容的 API,但不保证同步
StringBuilder也可以使用,其用法和StringBuffer 类似。
StringBuilder其实是和StringBuffer几乎一样, 只不过StringBuilder是非线程安全的
如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)
java8底层已经把字符串的+操作自动转换为StringBuilder的拼接操作;
StringBuilder的使用举例
StringBuilder wechat = new StringBuilder("Hollis");
String introduce = "每日更新Java 相关技术文章";
StringBuilder hollis = wechat.append(",").append(introduce);
String introduce = "每日更新Java 相关技术文章";
StringBuilder hollis = wechat.append(",").append(introduce);
StringBuilder是非线程安全的
线程非安全的
StringBuilder是非线程安全的容器
一般适用于单线程场景中的字符串拼接操作
String底层使用StringBuilder作为字符串拼接
为什么+号操作符,使用StringBuilder作为拼接条件,而不是使用StringBuffer呢?
原因是:加锁是一个比较耗时的操作,而加锁会影响性能
所以String底层使用StringBuilder作为字符串拼接。
String对象底层使用了StringBuilder对象的append方法,进行字符串拼接的
大量循环拼接字符串时注意事项
使用+连接符时, JVM会隐式创建StringBuilder对象, 这种方式在大部分情况下并不会造成效率的损失
不过,在进行大量循环拼接字符串时则需要注意
说明
这是一段很普通的代码,只不过对字符串s进行了+操作
通过反编译代码来看一下
通过反编译代码来看一下
你能看出来需要注意的地方了吗?
在每次进行循环时, 都会创建一个StringBuilder对象, 每次都会把一个新的字符串元素bbb拼接到aoa的后面
所以, 执行几次后的结果如下
执行几次后的结果
每次都会创建一个StringBuilder, 并把引用赋给StringBuilder对象
因此每个StringBuilder对象都是强引用, 这样在创建完毕后, 内存中就会多了很多StringBuilder的无用对象。
由于大量StringBuilder创建在堆内存中, 肯定会造成效率的损失
在这种情况下,建议在循环体外创建一个StringBuilder对象调用append() 方法手动拼接
建议在循环体外创建一个StringBuilder对象调用append() 方法手动拼接
这段代码中, 只会创建一个builder对象, 每次循环都会使用这个builder对象进行拼接, 因此提高了拼接效率
从源码角度看一下StringBuilder原理
首先来看一下StringBuilder的定义
StringBuilder被final修饰, 表示StringBuilder是不可被继承的
实际上, AbstractStringBuilder类,具体实现了可变字符序列的一系列操作
append
insert
delete
replace
charAt
StringBuilder使用两个变量作为元素
和String 类类似
StringBuilder 类也封装了一个字符数组
定义如下:char[] value;
char value; //存储字符数组
使用value表示存储的字符数组
与String 不同的是,它并不是final 的,所以他是可以修改的。
与String不同
字符数组中不一定所有位置都已经被使用,
它有一个实例变量,表示数组中已经使用的字符个数
使用count表示存储的字符串使用的计数
int count; //字符串使用的计数
定义如下:int count;
StringBuilder继承于AbstractStringBuilder
继承了AbstractStringBuilder 类
StringBuilder继承了AbstractStringBuilder 类
AbstractStringBuilder类中的两个变量
char value; //存储字符数组
int count; //字符串使用的计数
StringBuilder实现了2个接口
Serializable接口
序列化接口, 表示对象可以被序列化
CharSequence接口
字符串序列接口
CharSequence是一个可读的char值序列
提供了几个对字符序列进行只读访问的方法
length()
charAt(int index)
subSequence(int start,int end)
StringBuilder和StringBuffer也继承了这个接口
append 方法源码
其append 源码如下:
该类继承了AbstractStringBuilder 类,看下其append 方法:
append 会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。
StringBuilder和StringBuffer的扩容问题
StringBuffer的注意事项
虽然不产生新的中间String对象,但是内部有一个数组负责存储,提前设置合理大小,可以避免数组扩容引起的性能损耗。
StringBuilder的初始容量
StringBuilder的初始容量是16
当然也可以指定StringBuilder的初始容量,
在调用append拼接字符串, 会调用AbstractStringBuilder中的append方法
调用AbstractStringBuilder中的append方法
上面代码中有一个ensureCapacityInternal方法, 这个就是扩容方法
扩容方法ensureCapacityInternal方法
这个方法会进行判断, minimumCapacity就是字符长度+要拼接的字符串长度
如果拼接后的字符串要比当前字符长度大的话, 会进行数据的复制
真正扩容的方法是在newCapacity中
真正扩容的方法是在newCapacity中
扩容后的字符串长度会是原字符串长度增加一倍+2,
如果扩容后的长度还比拼接后的字符串长度小的话, 那就直接扩容到它需要的长度new Capacity=min Capacity, 然后再进行数组的拷贝
String、StringBuffer和StringBuilder三者选用的原则
1) 原则1
如果要操作少量的数据,用String
单线程操作大量数据,用StringBuilder
多线程操作大量数据,用StringBuffer
2) 原则2
不要使用String类的”+”来进行频繁的拼接,因为那样的性能极差的
应该使用StringBuffer或StringBuilder类,这在Java的优化上是一条比较重要的原则。
例如:
String result = "";
for (String s : hugeArray) {
result = result + s;
}
// 使用StringBuilder
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
sb.append(s);
}
String result = sb.toString();
for (String s : hugeArray) {
result = result + s;
}
// 使用StringBuilder
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
sb.append(s);
}
String result = sb.toString();
当出现上面的情况时,显然我们要采用第二种方法,因为第一种方法,每次循环都会创建一个String result用于保存结果,除此之外二者基本相同.
2) 原则3
StringBuilder一般使用在方法内部来完成类似”+”功能
因为是线程不安全的,所以用完以后可以丢弃。
StringBuffer主要用在全局变量中。
4)原则4
相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
而在现实的模块化编程中,负责某一模块的程序员不一定能清晰地判断该模块是否会放入多线程的环境中运行,
因此:除非确定系统的瓶颈是在 StringBuffer 上,并且确定你的模块不会运行在多线程模式下,才可以采用StringBuilder;否则还是用StringBuffer。
图解三者的区别
请你告诉我下面分别创建了几个对象?
问题1:s1创建了几个对象?
字符串在创建对象时,会在常量池中看有没有aaa这个字符串;
如果没有,此时还会在常量池中创建一个;
如果有则不创建,默认是没有的情况,所以会创建一个对象
下同,
问题2:s2创建了几个对象呢?
是两个对象还是一个对象?
使用javap -c,看一下反汇编代码
反汇编代码
编译器做了优化,String s2=“bbb"+“ccc”会直接被优化为bbbccc。
也就是,直接创建了一个bbbccc对象
javap -c
是什么?
jdk自带的反汇编工具
对代码进行反汇编操作
作用?
根据class字节码文件, 反汇编出当前类对应的code区(汇编指令) 、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
问题3:s3创建了几个对象呢?
使用javap -c来看
可以看到, s3执行+操作会创建一个StringBuilder对象然后执行初始化。
执行+号,相当于是执行new StringBuilder.append()操作
所以s3执行完成后,相当于创建了3个对象
所以s3执行完成后,相当于创建了3个对象
问题4:s4创建了几个对象?
在创建这个对象时,因为使用了new关键字, 所以肯定会在堆中创建一个对象。
然后会在常量池中看有没有aaa这个字符串;
如果没有,此时还会在常量池中创建一个;
如果有,则不创建,所以可能是创建一个或者两个对象,但是一定存在两个对象。
字符串拼接的几种方式和区别
字符串拼接是比较经常要做的事情,就是把多个字符串拼接到一起。
由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。
String字符串是恒定不可变的, 一旦创建出来就不会被修改,那么字符串拼接又是怎么回事呢?
字符串不变性与字符串拼接
其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。
下面一段字符串拼接代码:
String s = "abcd";
s = s.concat("ef");
s = s.concat("ef");
其实最后我们得到的s 已经是一个新的字符串了。
如下图:
其实最后我们得到的s 已经是一个新的字符串了。
s 中保存的是一个重新创建出来的String 对象的引用。
比较常用的五种在Java 种拼接字符串的方式
使用+拼接字符串
在Java 中,拼接字符串最简单的方式就是直接使用符号+来拼接。如:
String wechat = "Hollis";
String introduce = "每日更新Java 相关技术文章";
String hollis = wechat + "," + introduce;
String introduce = "每日更新Java 相关技术文章";
String hollis = wechat + "," + introduce;
特别说明
有人把Java 中使用+拼接字符串的功能理解为运算符重载。
其实并不是,Java 是不支持运算符重载的。
运算符重载
在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
这其实只是Java 提供的一个语法糖。
语法糖
语法糖(Syntactic sugar),也译为糖衣语法,
是由英国计算机科学家彼得·兰丁发明的一个术语,
指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。
语法糖让程序更加简洁,有更高的可读性。
Java 开发手册中不建议在循环体中使用+进行字符串拼接
Java 开发手册中不建议在循环体中使用+进行字符串拼接
使用+拼接字符串的实现原理
其实只是Java 提供的一个语法糖
String wechat = "Hollis";
String introduce = "每日更新Java 相关技术文章";
String hollis = wechat + "," + introduce;
把他生成的字节码进行反编译,看看结果。
String wechat = "Hollis";
String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6
587\u7AE0";//每日更新Java 相关技术文章
String hollis = (new StringBuilder()).append(wechat).append(",").append(int
roduce).toString();
String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6
587\u7AE0";//每日更新Java 相关技术文章
String hollis = (new StringBuilder()).append(wechat).append(",").append(int
roduce).toString();
通过查看反编译以后的代码,可以发现,
原来字符串常量在拼接过程中,是将String 转成了StringBuilder 后,使用其append 方法进行处理的。
J a v a 中的+ 对字符串的拼接, 其实现原理是使用StringBuilder.append。
Concat
使用String 类中的方法concat 方法来拼接字符串。
String wechat = "Hollis";
String introduce = "每日更新Java 相关技术文章";
String hollis = wechat.concat(",").concat(introduce);
concat 是如何实现的
concat 方法的源代码
这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,
再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String 对象并返回。
通过源码可以看到,经过concat 方法,其实是new 了一个新的String,这也就呼应到前面说的字符串的不变性问题上了。
StringBuffer
StringBuilder
StringUtils.join
除了JDK 中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,
如apache.commons 中提供的StringUtils 类,其中的join 方法可以拼接字符串。
String wechat = "Hollis";
String introduce = "每日更新Java 相关技术文章";
System.out.println(StringUtils.join(wechat, ",", introduce));
String introduce = "每日更新Java 相关技术文章";
System.out.println(StringUtils.join(wechat, ",", introduce));
StringUtils 中提供的join 方法,
最主要的功能是:
将数组或集合以某拼接符拼接到一起形成新的字符串
String []list ={"Hollis","每日更新Java 相关技术文章"};
String result= StringUtils.join(list,",");
System.out.println(result);
//结果:Hollis,每日更新Java 相关技术文章
实现原理
其实它是通过StringBuilder来实现的。
其实它是通过StringBuilder来实现的。
Java8中的String类中也提供了一个静态的join方法, 用法和StringUtils.join 类似。
效率比较
用时从短到长的对比
StringBuilder<StringBuffer<concat<+<StringUtils.join
StringBuilder
经过对比发现,直接使用StringBuilder 的方式是效率最高的。
因为它天生就是设计来定义可变字符串和字符串的变化操作的。
StringBuffer
在StringBuilder 的基础上,做了同步处理,
所以在耗时上会相对多一些。
StringUtils.join
也是使用了StringBuilder,
并且其中还是有很多其他操作,所以耗时较长,这个也容易理解
更擅长处理字符串数组或者列表的拼接。
使用+拼接字符串
其实使用+拼接字符串的实现原理也是使用的StringBuilder
那为什么结果相差这么多,高达1000 多倍呢?
源代码
反编译后
可以看到反编译后的代码,
在for 循环中,每次都是new 了一个StringBuilder,然后再把String 转成StringBuilder,再进行append。
而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。
所以, Java 开发手册建议:循环体内,字符串的连接方式,使用StringBuilder 的append 方法进行扩展。而不要使用+。
其他版本
某些特别情况下, String 对象的字符串拼接其实是被 Java Compiler 编译成了 StringBuffer 对象的拼接,
所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢
例如:
String s1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
生成 String s1对象的速度并不比 StringBuffer慢。
其实在Java Compiler里,自动做了如下转换:
Java Compiler直接把上述第一条语句编译为:
String s2 = “This is only a”;
String s3 = “ simple”;
String s4 = “ test”;
String s1 = s2 + s3 + s4;
String s3 = “ simple”;
String s4 = “ test”;
String s1 = s2 + s3 + s4;
Java Compiler会规规矩矩的按照原来的方式去做,
String的concatenation(即+)操作利用了StringBuilder(或StringBuffer)的append方法实现,此时,
对于上述情况,若s2,s3,s4采用String定义,拼接时需要额外创建一个StringBuffer(或StringBuilder),之后将StringBuffer转换为String;
若采用StringBuffer(或StringBuilder),则不需额外创建StringBuffer。
强调下
1、如果不是在循环体中进行字符串拼接的话,直接使用+就好了。
2 、如果在并发场景中进行字符串拼接, 要使用StringBuffer来代替StringBuilder。
图解区别
子主题
StringBuffer,StringBuilder底层代码,提出来提前设置大小的优化建议
(1)字符串缓存
最新的jdk8,使用G1 GC,指定字符串排重参数可以达到底层去重的效果:
-XX:+UseStringDedumplication
(字符串缓存是提高存储效率的中重要途径,最好使用jvm的参数进行调优。)
把jvm的堆转储,(dump heap)发现半数是重复的
如果可以避免重复的字符串,可以有效降低内存消耗和对象创建开销。
可以通过jvm的参数设置缓存池的大小 -XX:StringTableSize=x
intern是一种显示的排重机制,但是不推荐使用;
最新的jdk8,使用G1 GC,指定字符串排重参数可以达到底层去重的效果:
-XX:+UseStringDedumplication
(2)其它字符串的底层优化
比如使用本地内联方法的intrinsic机制来优化字符串的操作速度;
改变StringBuilder的存储的数据结构为Compact Strings提高存储效率;
String、StringBuffer和StringBuilder常见面试题
String、StringBuffer、StringBuilder的区别?+1
String、StringBuffer、StringBuilder的线程安全问题?
String类的常用API/方法有哪些?
value of ()
value of 与new 的区别 ?
String类有什么特点?
String是final类型,因此不断继承这个类,不能修饰这个类。
为了提高效率节省空间,应该用StringBuffer。
String不是基本类型
== 与equals的区别?
StringTokenizer类
StringTokenizer类在java.util包中
java.util.Tokenizer JDK 1.0 or later
StringTokenizer 类允许应用程序将字符串分解为标记。
在新代码中并不鼓励使用它
StringTokenizer 是出于兼容性的原因,而被保留的遗留类。
(虽然在新代码中并不鼓励使用它)
建议所有寻求此功能的人使用 String 的 split 方法或 java.util.regex 包。
有两个常用的构造方法
StringTokenizer(String s)
为字符串s构造一个分析器。
使用默认的分隔标记,即空格符(若干个空格被看做一个空格)、换行符、回车符、Tab符、进纸符。
StringTokenizer(String s, String delim)
为字符串s构造一个分析器。
参数dilim中的字符,被作为分隔标记
StringTokenizer对象称作一个字符串分析器,可以使用下列方法
nextToken( )
逐个获取字符串中的语言符号(单词),字符串分析器中的负责计数的变量的值就自动减一 。
hasMoreTokens( )
只要字符串中还有语言符号,即计数变量的值大于0,
该方法就返回true,否则返回false。
countTokens( )
得到分析器中计数变量的值。
使用String.split分割字符串对比与StringTokenizer分割字符串
使用String.split分割字符串,注意处理分隔符中的一些特殊字符
java.lang.String 的 split() 方法, JDK 1.4 or later
异常的代码示例
public String[] split(String regex,int limit)
public class StringSplit {
public static void main(String[] args) {
String sourceStr = "1,2,3,4,5";
String[] sourceStrArray = sourceStr.split(",");
for (int i = 0; i < sourceStrArray.length; i++) {
System.out.println(sourceStrArray[i]);
}
// 最多分割出3个字符串
int maxSplit = 3;
sourceStrArray = sourceStr.split(",", maxSplit);
for (int i = 0; i < sourceStrArray.length; i++) {
System.out.println(sourceStrArray[i]);
}
}
}
public class StringSplit {
public static void main(String[] args) {
String sourceStr = "1,2,3,4,5";
String[] sourceStrArray = sourceStr.split(",");
for (int i = 0; i < sourceStrArray.length; i++) {
System.out.println(sourceStrArray[i]);
}
// 最多分割出3个字符串
int maxSplit = 3;
sourceStrArray = sourceStr.split(",", maxSplit);
for (int i = 0; i < sourceStrArray.length; i++) {
System.out.println(sourceStrArray[i]);
}
}
}
预期外的输出结果:
1
2
3
4
5
1
2
3,4,5
2
3
4
5
1
2
3,4,5
进一步解释说明
split 的实现直接调用的 matcher 类的 split 的方法。
在使用String.split方法分隔字符串时,分隔符如果用到一些特殊字符,可能会得不到预期的结果。
在正则表达式中有特殊的含义的字符,使用时,必须进行转义,
转义后的代码示例:
public class StringSplit {
public static void main(String[] args) {
String value = "192.168.128.33";
// 注意要加\\,要不出不来,yeahString[] names = value.split("\\.");
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
}
}
public static void main(String[] args) {
String value = "192.168.128.33";
// 注意要加\\,要不出不来,yeahString[] names = value.split("\\.");
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
}
}
split分隔符总结
字符"|","*","+"都得加上转义字符,前面加上"\\"。
而如果是"\",那么就得写成"\\\\"。
如果一个字符串中有多个分隔符,可以用"|"作为连字符。
比如:String str = "Java string-split#test",可以用Str.split(" |-|#")把每个字符串分开。这样就把字符串分成了3个子字符串。
使用StringTokenizer分割字符串
样例代码
public class StringSplit {
public static void main(String[] args) {
String ip = "192.168.128.33";
StringTokenizer token=new StringTokenizer(ip,".");
while(token.hasMoreElements()){
System.out.print(token.nextToken()+" ");
}
}
}
public static void main(String[] args) {
String ip = "192.168.128.33";
StringTokenizer token=new StringTokenizer(ip,".");
while(token.hasMoreElements()){
System.out.print(token.nextToken()+" ");
}
}
}
但是StringTokenizer对于字符串"192.168..33"的分割,返回的字符串数组只有3个元素,对于两个分隔符之间的空字符串会忽略,这个要慎重使用。
但是String.split用的都是按顺序遍历的算法,时间复杂度O(m*n)较高
(String.split是用正则表达式匹配,所以不使用KMP字符串匹配算法)
性能上,StringTokenizer好很多,对于频繁使用字符串分割的应用,例如etl数据处理,使用StringTokenizer性能可以提高很多。
Comparable和Comparator的对比(重点)
Comparable接口
Comparable是一个排序接口
list或者数组,实现了这个接口,能够自动的进行排序
相关类的方法有
Collections.sort()
Arrays.sort()
此接口给实现类提供了一个排序的方法
public int compareTo(T o)
compareTo方法,接受任意类型的参数, 来进行比较
compareTo方法与equals方法的比较
compareTo方法不同于equals方法,
compareTo方法的返回值是一个int类型
compareTo方法的返回值是一个int类型
而equals方法返回的是boolean类型
而equals方法返回的是boolean类型
compareTo(T o)方法抛出异常
NullPointerException:如果对象o为null, 抛出空指针异常
ClassCastException:如果需要类型转换之后进行比较, 可能会抛出ClassCastException
SortedMap接口的key,内置了compareTo方法,来进行键排序
SortedSet接口,也是内置了compareTo方法,作为其内部元素的比较手段
Comparator接口
对Comparator的解释
更像是一个内部排序接口
Comparator相当于一个比较器, 作用和Comparable类似
也可以对SortedMap和SortedSet的数据结构进行精准的控制
你可以不用实现此接口或者Comparable接口就可以实现次序比较。
Tree Set和TreeMap的数据结构底层也是使用Comparator来实现。
不同于Comparable, 比较器可以任选地允许比较null参数, 同时保持要求等价关系。
也是使用以下方法来进行排序
Collections.sort()
Arrays.sort()
一个类,实现了Comparable比较器, 就意味着它本身支持排序
样例代码1
代码段1
代码段2
输出:
false true false false
样例代码2
继承comparator接口,重写compare方法
使用collections.sort进行排序
也可以使用Arrays.sort 0进行排序, 不过针对的数据结构是数组。
Comparator比较器的方法
int compare(To 1, To 2)
这个方法不允许进行null值比较,会抛出空指针异常
用法和Comparable的compareTo 用法基本一样
Comparable的compareTo(T o)方法抛出异常
NullPointerException:如果对象o为null, 抛出空指针异常
ClassCastException:如果需要类型转换之后进行比较, 可能会抛出ClassCastException
boolean equals(Object obj)
jdk 1.8之后又增加了很多新的方法
jdk 1.8之后又增加了很多新的方法
代码实现
Comparable和Comparator的对比
1、Comparable
更像是自然排序
对于一些普通的数据类型 ,它们默认实现了Comparable接口, 实现了compareTo方法,可以直接使用
2、Comparator
更像是定制排序
同时存在时采用Comparator(定制排序) 的规则进行比较。
对于一些自定义类, 它们可能在不同情况下需要实现不同的比较策略, 可以新创建Comparator接口, 然后使用特定的Comparator实现进行比较。
Runtime类运行可执行文件
Runtime类在java.lang包
用Runtime 类声明一个对象
Runtime ec;
使用该类的getRuntime()静态方法创建这个对象
ec=Runtime.getRuntime();
ec可以调用exec方法打开本地机的可执行文件或执行一个操作
exec(String command)
Runtime对象打开Windows平台上的记事本程序和浏览器。
Scanner类和System类
System类
java.lang包中的System类中有许多类方法,
这些方法用于设置和Java虚拟机相关的数据.
System类中的exit()方法
如果一个Java程序希望立刻关闭运行当前程序的Java虚拟机,那么就可以让System类调用exit(int status),并向该方法的参数传递数字0或非0的数字。
传递数字0表示是正常关闭虚拟机,否则表示非正常关闭虚拟机。
从命令行输入、输出数据
输入基本型数据,使用Scanner类
可以使用Scanner类创建一个对象
Scanner reader=new Scanner(System.in);
reader对象调用下列方法,读取用户在命令行(例如,MS-DOS窗口)输入的各种基本类型数据
nextBoolean()
nextByte()
nextShort()
nextInt()
nextLong()
nextFloat()
nextDouble()
上述方法执行时都会堵塞,程序等待用户在命令行输入数据回车确认。
输出基本型数据,使用System类
可用两种方法输出串值、表达式的值
System.out.println()
输出数据后换行
System.out.print()
输出数据后不换行
允许使用并置符号:“+”将变量、表达式或一个常数值与一个字符串并置一起输出
System.out.println(m+"个数的和为"+sum);
System.out.println(“:”+123+“大于”+122)
JDK1.5新增了和C语言中printf函数类似的数据输出方法
该方法使用格式如下
System.out.printf("格式控制部分",表达式1,表达式2,…表达式n)
格式控制部分由格式控制符号:%d、%c、%f、%s和普通的字符组成,普通字符原样输出。
格式符号用来输出表达式的值
%d:输出int类型数据值
%c:输出char型数据
%f:输出浮点型数据,小数部分最多保留6位
%s:输出字符串数据
输出数据时也可以控制数据在命令行的位置
例如:
%md:输出的int型数据占m列
%m.nf:输出的浮点型数据占m列,小数点保留n位。
使用Scanner类和正则表达式来解析文件
1. 使用默认分隔标记解析文件
创建Scanner对象,并指向要解析的文件
File file = new File("hello.java");
Scanner sc = new Scanner(file);
Scanner sc = new Scanner(file);
sc将空白作为分隔标记 ,相关方法
next() 依次返回file中的单词
hasNext() 判断file最后一个单词是否已被next()方法返回
hasNext() 判断file最后一个单词是否已被next()方法返回
2.使用正则表达式作为分隔标记解析文件
创建Scanner对象,指向要解析的文件,并使用useDelimiter方法指定正则表达式作为分隔标记
File file = new File("hello.java");
Scanner sc = new Scanner(file);
sc.useDelimiter(正则表达式); sc将正则表达式作为分隔标记
Scanner sc = new Scanner(file);
sc.useDelimiter(正则表达式); sc将正则表达式作为分隔标记
相关方法
next() 依次返回file中的单词
hasNext() 判断file最后一个单词是否已被next()方法返回
hasNext() 判断file最后一个单词是否已被next()方法返回
使用Scanner类从字符串中,解析程序所需要数据
1.使用默认分隔标记解析字符串
创建Scanner对象,并将要解析的字符串传递给所构造的对象
例如 String NBA = "I Love This Game";
如下构造一个Scanner对象
Scanner scanner = new Scanner(NBA);
那么scanner将空格作为分隔标记、调用next()方法依次返回NBA中的单词,
如果NBA最后一个单词已被next()方法返回,
scanner调用hasNext()将返回false,否则返回true。
2.使用正则表达式作为分隔标记解析字符串
Scanner对象可以调用useDelimiter(正则表达式);
方法将一个正则表达式作为分隔标记,
即和正则表达式匹配的字符串都是分隔标记。
Java中从控制台输入数据的几种常用方法
使用标准输入串System.in
//System.in.read()一次只读入一个字节数据
//通常要取得一个字符串或一组数字
//System.in.read()返回一个整数
//必须初始化
//通常要取得一个字符串或一组数字
//System.in.read()返回一个整数
//必须初始化
//int read = 0;
char read = '0';
System.out.println("输入数据:");
try {
//read = System.in.read();
read = (char) System.in.read();
}catch(Exception e){
e.printStackTrace();
}
System.out.println("输入数据:"+read);
char read = '0';
System.out.println("输入数据:");
try {
//read = System.in.read();
read = (char) System.in.read();
}catch(Exception e){
e.printStackTrace();
}
System.out.println("输入数据:"+read);
使用Scanner取得一个字符串或一组数字
在新增一个Scanner对象时,需要一个System.in对象,因为实际上还是System.in在取得用户输入。
Scanner的方法:
* next()用以取得用户输入的字符
* nextInt()将取得的输入字符串转换为整数类型
* nextFloat()转换成浮点型
* nextBoolean()转换成布尔型
* nextInt()将取得的输入字符串转换为整数类型
* nextFloat()转换成浮点型
* nextBoolean()转换成布尔型
Scanner的使用demo
import java.util.Scanner;
public class Main {
static int max = 0;
static int m = 0;
static int n = 0;
static int arr[][];
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
n = cin.nextInt();
m = cin.nextInt();
arr = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
arr[i][j] = cin.nextInt();
}
}
find(0, 0,0);
System.out.println(max);
}
public static int find(int nn, int mm,int tmp) {
if(nn>=n||mm>=m){
return 0;
}
tmp = tmp + arr[nn][mm];
if (max < tmp) {
max = tmp;
}
find(nn, mm + 1,tmp);
find(nn + 1, mm,tmp);
return tmp;
}
/*
* 4 5 0 0 8 0 0 0 0 0 9 0 0 7 0 0 0 0 0 6 0 0
*/
}
public class Main {
static int max = 0;
static int m = 0;
static int n = 0;
static int arr[][];
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
n = cin.nextInt();
m = cin.nextInt();
arr = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
arr[i][j] = cin.nextInt();
}
}
find(0, 0,0);
System.out.println(max);
}
public static int find(int nn, int mm,int tmp) {
if(nn>=n||mm>=m){
return 0;
}
tmp = tmp + arr[nn][mm];
if (max < tmp) {
max = tmp;
}
find(nn, mm + 1,tmp);
find(nn + 1, mm,tmp);
return tmp;
}
/*
* 4 5 0 0 8 0 0 0 0 0 9 0 0 7 0 0 0 0 0 6 0 0
*/
}
使用BufferedReader取得含空格的输入
//Scanner取得的输入以space, tab, enter 键为结束符,
//要想取得包含space在内的输入,可以用java.io.BufferedReader类来实现
//使用BufferedReader的readLine( )方法
//必须要处理java.io.IOException异常
//要想取得包含space在内的输入,可以用java.io.BufferedReader类来实现
//使用BufferedReader的readLine( )方法
//必须要处理java.io.IOException异常
BufferedReader br = new BufferedReader(new InputStreamReader(System.in ));
//java.io.InputStreamReader继承了Reader类
String read = null;
System.out.print("输入数据:");
try {
read = br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("输入数据:"+read);
//java.io.InputStreamReader继承了Reader类
String read = null;
System.out.print("输入数据:");
try {
read = br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("输入数据:"+read);
例子
package String;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class BeautyDegree {
public static void main(String[] args) {
// 获取用户的输入
BufferedReader mBufferedReader = new BufferedReader(new InputStreamReader(System.in));
// 行数
String line;
int timer = 0;
boolean mFlag = true;
StringBuilder mStringBuilder = null;
try {
while ((line = mBufferedReader.readLine()) != null) {
if (mFlag) {
timer = Integer.parseInt(line);
mFlag = false;
mStringBuilder = new StringBuilder();
continue;
}
mStringBuilder.append(line + ',');
timer--;
if (timer == 0) {
mFlag = true;
outPutBeauty(mStringBuilder.toString());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void outPutBeauty(String string) {
String[] result = string.split(",");
for (String tmp : result) {
System.out.println(getBeauty(tmp));
}
}
public static int getBeauty(String name) {
char[] chs = name.toLowerCase().toCharArray();
int[] target = new int[26];
for (int i = 0; i < chs.length; i++) {
target[chs[i] - 'a']++;
}
Arrays.sort(target);
int res = 0;
for (int i = 25; i >= 0; i--) {
res += target[i] * (i + 1);
}
return res;
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class BeautyDegree {
public static void main(String[] args) {
// 获取用户的输入
BufferedReader mBufferedReader = new BufferedReader(new InputStreamReader(System.in));
// 行数
String line;
int timer = 0;
boolean mFlag = true;
StringBuilder mStringBuilder = null;
try {
while ((line = mBufferedReader.readLine()) != null) {
if (mFlag) {
timer = Integer.parseInt(line);
mFlag = false;
mStringBuilder = new StringBuilder();
continue;
}
mStringBuilder.append(line + ',');
timer--;
if (timer == 0) {
mFlag = true;
outPutBeauty(mStringBuilder.toString());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void outPutBeauty(String string) {
String[] result = string.split(",");
for (String tmp : result) {
System.out.println(getBeauty(tmp));
}
}
public static int getBeauty(String name) {
char[] chs = name.toLowerCase().toCharArray();
int[] target = new int[26];
for (int i = 0; i < chs.length; i++) {
target[chs[i] - 'a']++;
}
Arrays.sort(target);
int res = 0;
for (int i = 25; i >= 0; i--) {
res += target[i] * (i + 1);
}
return res;
}
}
Date类构造Date对象
Date类在java.util包中
1.使用Date类的无参数构造方法创建的对象可以获取本地当前时间。
例: Date nowTime=new Date();
2.使用Date类的无参数构造方法
Date(long time)
例:Date date1=new Date(1000),
date2=new Date(-1000);
date2=new Date(-1000);
Date对象表示时间的默认顺序是
星期、月、日、小时、分、秒、年。
例如:Tue Aug 04 08:59:32 CST 2009。
SimpleDateFormat日期格式化
SimpleDateFormat的使用
服务器返回的值往往是秒,但是计算的时候,要求毫秒,需要*1000L才能得到正确的日期结果。
//制定日期的显示格式
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss");
//m为从服务器返回的数据转换后的值(往往是将服务器返回的字符串形式的值,需要转化为int型或者long型)
String time=sdf.format(new Date((m*1000L));
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss");
//m为从服务器返回的数据转换后的值(往往是将服务器返回的字符串形式的值,需要转化为int型或者long型)
String time=sdf.format(new Date((m*1000L));
格林时间,是以1970-01-01 00:00:00为基准计起的,服务器返回的,就是某一时刻到格林时间基准的秒数(如果是毫秒那就更好了,直接使用不用*1000L)拿到上面的time,比真正的时间少了八个小时,
解决的办法:把JAVA默认的时区,改为东八区.
public String paserTime(int time){
System.setProperty("user.timezone", "Asia/Shanghai");
TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone.setDefault(tz); //把默认时区改成我们的时区:
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String times = format.format(new Date(time * 1000L));
System.out.print("日期格式---->" + times);
return times;
}
System.setProperty("user.timezone", "Asia/Shanghai");
TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone.setDefault(tz); //把默认时区改成我们的时区:
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String times = format.format(new Date(time * 1000L));
System.out.print("日期格式---->" + times);
return times;
}
使用java.util包中的DateFormat的子类SimpleDateFormat来实现日期的格式化。
SimpleDateFormat有一个常用构造方法
public SimpleDateFormat(String pattern);
该构造方法可以用参数pattern指定的格式创建一个对象
pattern是由普通字符和一些称作格式符组成的字符序列。
例如,假如当前时间是2009年10月11日星期日
设pattern= "yyyy-MM-dd", 那么使用pattern格式化后的时间就是:2009-10-11
该对象调用: public String format(Date date) 方法格式化时间对象date。
NumberFormat日期格式化
使用NumberFormat,实现四舍五入
private String format(double num){
NumberFormat formatter = new DecimalFormat("0.00");
String s = formatter.format(num);
return s;
}
NumberFormat formatter = new DecimalFormat("0.00");
String s = formatter.format(num);
return s;
}
Calendar类
Calendar类在java.util包中
使用Calendar类的static方法 getInstance()可以初始化一个日历对象
如: Calendar calendar= Calendar.getInstance();
calendar对象可以调用方法,将日历翻到任何一个时间
public final void set(int year,int month,int date)
public final void set(int year, int month, int date, int hour,int minute)
public final void set(int year,int month, int date, int hour, int minute,int second)
public final void set(int year, int month, int date, int hour,int minute)
public final void set(int year,int month, int date, int hour, int minute,int second)
calendar对象常用方法
public int get(int field) 可以获取有关年份、月份、小时、星期等信息
public long getTimeInMillis() 可以将时间表示为毫秒。
BigInteger类
属于java.math.BigInteger
BigInteger属于java.math.BigInteger
在每次使用前都要import 这个类。偶开始就忘记import了,于是总提示找不到提示符。
java.math包中的BigInteger类
BigInteger类模拟了所有的int型数学操作
提供任意精度的整数运算
如add()==“+”,divide()==“-”等
但注意其内容进行数学运算时不能直接使用数学运算符进行运算,必须使用其内部方法。
而且其操作数也必须为BigInteger型。
如:two.add(2)就是一种错误的操作,因为2没有变为BigInteger型。
构造方法
public BigInteger(String val)
构造一个十进制的BigInteger对象。
将 BigInteger 的十进制字符串表示形式转换为 BigInteger。
BigInteger(String val, int radix)
将指定基数的 BigInteger 的字符串表示形式转换为 BigInteger。
如要将int型的2转换为BigInteger型,要写为BigInteger two=new BigInteger("2"); //注意2双引号不能省略
BigInteger类的常用类方法
public BigInteger add(BigInteger val)返回当前大整数对象与参数指定的大整数对象的和。
public BigInteger subtract(BigInteger val)返回当前大整数对象与参数指定的大整数对象的差。
public BigInteger multiply(BigInteger val)返回当前大整数对象与参数指定的大整数对象的积。
public BigInteger divide(BigInteger val)返回当前大整数对象与参数指定的大整数对象的商。
public BigInteger remainder(BigInteger val)返回当前大整数对象与参数指定的大整数对象的余。
public BigInteger abs() 返回当前大整数对象的绝对值。
public BigInteger pow(int a) 返回当前大整数对象的a次幂。
public String toString() 返回当前大整数对象十进制的字符串表示。
public String toString(int p) 返回当前大整数对象p进制的字符串表示。
public BigInteger subtract(BigInteger val)返回当前大整数对象与参数指定的大整数对象的差。
public BigInteger multiply(BigInteger val)返回当前大整数对象与参数指定的大整数对象的积。
public BigInteger divide(BigInteger val)返回当前大整数对象与参数指定的大整数对象的商。
public BigInteger remainder(BigInteger val)返回当前大整数对象与参数指定的大整数对象的余。
public BigInteger abs() 返回当前大整数对象的绝对值。
public BigInteger pow(int a) 返回当前大整数对象的a次幂。
public String toString() 返回当前大整数对象十进制的字符串表示。
public String toString(int p) 返回当前大整数对象p进制的字符串表示。
public int compareTo(BigInteger val)
返回当前大整数对象与参数指定的大整数的比较结果,
返回值是1、-1或0,分别表示当前大整数对象大于、小于或等于参数指定的大整数。
将此 BigInteger 与指定的 BigInteger 进行比较。
对于针对六个布尔比较运算符 (<, ==, >, >=, !=, <=) 中的每一个运算符的各个方法,优先提供此方法。
执行这些比较的建议语句是:(x.compareTo(y) <op> 0),其中 <op> 是六个比较运算符之一。
指定者: 接口 Comparable<BigInteger> 中的 compareTo
参数: val - 将此 BigInteger 与之比较的 BigInteger。
返回:
指定者: 接口 Comparable<BigInteger> 中的 compareTo
参数: val - 将此 BigInteger 与之比较的 BigInteger。
返回:
BigInteger remainder(BigInteger val)
返回其值为 (this % val) 的 BigInteger。
remainder用来求余数。
BigInteger negate()
negate将操作数变为相反数。
返回其值是 (-this) 的 BigInteger。
当要把计算结果输出时应该使用.toString方法将其转换为10进制的字符串
String toString()
返回此 BigInteger 的十进制字符串表示形式。
输出方法:System.out.print(two.toString());
将BigInteger的数转为2进制
public class TestChange {
public static void main(String[] args) {
System.out.println(change("3",10,2));
}
//num 要转换的数 from源数的进制 to要转换成的进制
private static String change(String num,int from, int to){
return new java.math.BigInteger(num, from).toString(to);
}
}
public static void main(String[] args) {
System.out.println(change("3",10,2));
}
//num 要转换的数 from源数的进制 to要转换成的进制
private static String change(String num,int from, int to){
return new java.math.BigInteger(num, from).toString(to);
}
}
DecimalFormat类
DecimalFormat类在java.text包中。
可以用DecimalFormat类对输出的数字结果进行必要的格式化 。
格式化数字
格式化整数位和小数位
DecimalFormat对象调用:public final String format(double number)对参数指定的数字进行格式化,并将格式化结果以String对象返回。
例如:DecimalFormat format=new DecimalFormat("00000.00");
那么 String result=format.format(6789.8765);得到的result是:“06789.88”
那么 String result=format.format(6789.8765);得到的result是:“06789.88”
整数位的分组
当希望将数字的整数部分分组(用逗号分隔),可以在DecimalFormat对象中的
例如:将 “123456789.9876543” 的整数部分按4位分组的一个格式化模式是:“#,##,###,##00.00”
使用该模式格式化上述数字的结果是:1,2345,6789.99
格式化为百分数或千分数
在DecimalFormat对象中的数字格式化模式尾加“%”,可以将数字格式化为百分数、尾加“\u2030”将数字格式化为千分数。
格式化为科学计数
在DecimalFormat对象中的数字格式化模式尾加“E0”,可以将数字格式化为科学计数。
格式化为货币值
在DecimalFormat对象中的数字格式化模式尾加货币符号,例如“$”“¥”,可以将数字格式化为带货币符号的串。
将格式化字符串转化为数字
根据要转化的字符串创建一个DecimalFormat对象,并将适合该字符串的格式化模式传递给该对象
例如: DecimalFormat df = new DecimalFormat("###,#00.000$");
那么,df调用parse(String s)方法将返回一个Number对象,
例如:Number num = df.parse("3,521,563.345$");
那么,Number对象调用方法可以返回该对象中含有的数字,
例如:double d=number.doubleValue(); d的值是3521563.345。
例如:Number num = df.parse("3,521,563.345$");
那么,Number对象调用方法可以返回该对象中含有的数字,
例如:double d=number.doubleValue(); d的值是3521563.345。
Pattern类
Java提供了专门用来进行模式匹配的Pattern类和Match类
这些类在java.util.regex包中
模式对象
使用Pattern类创建一个对象,称作模式对象,模式对象是对正则表达式的封装。
Pattern类调用类方法compile(String regex)返回一个模式对象,其中的参数regex是一个正则表达式,称作模式对象使用的模式。
例如: Pattern p = Pattern.compile("hello\\d");
Pattern类也可以调用类方法compile(String regex, int flags)返回一个Pattern对象 。
模式对象p调用matcher(CharSequence input)方法返回一Matcher对象m,称作匹配对象。
Matcher对象m常用的方法
public boolean find() :寻找input和regex匹配的下一子序列
public boolean matches():判断input是否完全和regex匹配
public boolean lookingAt():判断从input的开始位置是否有和regex匹配的子序列
public boolean find(int start):判断input从参数start指定位置开始是否有和regex匹配的子序列
public String replaceAll(String replacement):Matcher对象m调用该方法可以返回一个字符串
public String replaceFirst(String replacement):Matcher对象m调用该方法可以返回一个字符串
public boolean matches():判断input是否完全和regex匹配
public boolean lookingAt():判断从input的开始位置是否有和regex匹配的子序列
public boolean find(int start):判断input从参数start指定位置开始是否有和regex匹配的子序列
public String replaceAll(String replacement):Matcher对象m调用该方法可以返回一个字符串
public String replaceFirst(String replacement):Matcher对象m调用该方法可以返回一个字符串
Math类
Math类在java.lang包中。
Math类包含许多用来进行科学计算的类方法,这些方法可以直接通过类名调用。
Math类有两个静态常量
E 2.7182828284590452354
PI 3.14159265358979323846
Math类的常用类方法
public static long abs(double a) 返回a的绝对值。
public static double max(double a,double b) 返回a、b的最大值。
public static double min(double a,double b) 返回a、b的最小值。
public static double random() 产生一个0到1之间的随机数(不包括0和1)
public static double pow(double a,double b) 返回a的b次幂。
public static double sqrt(double a) 返回a的平方根。
public static double log(double a) 返回a的对数。
public static double sin(double a) 返回正弦值。
public static double asin(double a) 返回反正弦值。
public static double max(double a,double b) 返回a、b的最大值。
public static double min(double a,double b) 返回a、b的最小值。
public static double random() 产生一个0到1之间的随机数(不包括0和1)
public static double pow(double a,double b) 返回a的b次幂。
public static double sqrt(double a) 返回a的平方根。
public static double log(double a) 返回a的对数。
public static double sin(double a) 返回正弦值。
public static double asin(double a) 返回反正弦值。
与Math有关的计算题
子主题
子主题
首先要注意的是它的返回值类型是long,如果 Math.round(11.5f),那它的返回值类型就是int,这一点可以参考API
其次 Returns the closest long to the argument, with ties rounding to positive infinity
它返回的是一个最接近参数的long 值(例如: Math.round(11.6) = 12; Math.round(-11.6) = -12; Math.round(-0.1) = 0; Math.round(0.1) = 0 ),那如果出现向上向下距离一样的数值, 比如题目中的11.5,该如何处理呢 ,别着急,看它的后半句话, with ties rounding to positive infinity( 同时向正无穷方向取舍或者翻译成取较大的值,英语水平较差,只能翻译成这样了;
例子: Math.round(11.5) ,首先与 11.5最接近的有两个整数 11 和 12,取较大的那结果就是12;
Math.round(-11.5), 首先与 -11.5最接近的有两个整数 -11 和 -12,取较大的那结果就是-11;
Math.round(0.5), 首先与 0.5最接近的有两个整数 0 和 1,取较大的那结果就是1;
Math.round(-0.5), 首先与 -0.5最接近的有两个整数 -1 和 0,取较大的那结果就是0; )
然后它有三个特例:
1. 如果参数为 NaN(无穷与非数值) ,那么结果为 0。
2.如果参数为负无穷大或任何小于等于 Long.MIN_VALUE 的值,那么结果等于Long.MIN_VALUE 的值。
3.如果参数为正无穷大或任何大于等于 Long.MAX_VALUE 的值,那么结果等于Long.MAX_VALUE 的值。
最后 最好还是看一下API或者源码,不要信了我的邪
向上取整
round方法,它表示“四舍五入”,算法为Math.floor(x+0.5),即将原来的数字加上0.5后再向下取整,
所以,Math.round(11.5)的结果为12,Math.round(-11.5)的结果为-11。 ceil是天花板,向上取整。 floor是地板,向下去整。
其次 Returns the closest long to the argument, with ties rounding to positive infinity
它返回的是一个最接近参数的long 值(例如: Math.round(11.6) = 12; Math.round(-11.6) = -12; Math.round(-0.1) = 0; Math.round(0.1) = 0 ),那如果出现向上向下距离一样的数值, 比如题目中的11.5,该如何处理呢 ,别着急,看它的后半句话, with ties rounding to positive infinity( 同时向正无穷方向取舍或者翻译成取较大的值,英语水平较差,只能翻译成这样了;
例子: Math.round(11.5) ,首先与 11.5最接近的有两个整数 11 和 12,取较大的那结果就是12;
Math.round(-11.5), 首先与 -11.5最接近的有两个整数 -11 和 -12,取较大的那结果就是-11;
Math.round(0.5), 首先与 0.5最接近的有两个整数 0 和 1,取较大的那结果就是1;
Math.round(-0.5), 首先与 -0.5最接近的有两个整数 -1 和 0,取较大的那结果就是0; )
然后它有三个特例:
1. 如果参数为 NaN(无穷与非数值) ,那么结果为 0。
2.如果参数为负无穷大或任何小于等于 Long.MIN_VALUE 的值,那么结果等于Long.MIN_VALUE 的值。
3.如果参数为正无穷大或任何大于等于 Long.MAX_VALUE 的值,那么结果等于Long.MAX_VALUE 的值。
最后 最好还是看一下API或者源码,不要信了我的邪
向上取整
round方法,它表示“四舍五入”,算法为Math.floor(x+0.5),即将原来的数字加上0.5后再向下取整,
所以,Math.round(11.5)的结果为12,Math.round(-11.5)的结果为-11。 ceil是天花板,向上取整。 floor是地板,向下去整。
使用Console流,读取密码
使用Console流读取密码
作用:可以读取用户在命令行输入的密码,而且用户在命令行输入的密码不会显示在命令行中。
JDK1.6版本在java.io包中新增
使用步骤
首先使用System类调用console()方法返回一个Console流:
Console cons = System.console();
然后,Console流调用readPassword()方法读取用户在键盘输入的密码,并将密码以一个char数组返回:
char[] passwd = cons.readPassword();
文件锁FileLock 和 FileChannel类
两个处理Java提供的文件锁功能
FileLock
在java.nio包中
FileChannel类
在java.nio.channels包中
输入、输出流读写文件时,可以使用文件锁。
RondomAccessFile创建的流,在读写文件时,使用文件锁的步骤如下
1.先使用RondomAccessFile流建立指向文件的流对象,该对象的读写属性必须是rw
例如: RandomAccessFile input= new RandomAccessFile("Example.java","rw");
2.Input流调用方法getChannel()获得一个连接到地层文件的FileChannel对象(信道)
FileChannel channel=input.getChannel();
3.信道调用tryLock()或lock()方法获得一个FileLock(文件锁)对象,这一过程也称作对文件加锁
例如: FileLock lock=channel.tryLock();
使用场景
Java程序让用户从键盘输入一个正整数,然后程序读取文件的内容,如输入整数2,程序顺序读取文件中2行的内容。 程序首先释放文件锁,然后读取文件内容,读取之后立刻给文件加锁,等待用户输入下一个整数。因此,在用户输入下一个整数之前,其他程序无法操作被当前用户加锁的文件,如其他用户无法使用Windows操作系统提供的“记事本”程序(Notepad.exe)无法保存被当前Java程序加锁的文件。
ThreadLocalRandom 与Random类
Random
产生一个伪随机数(通过相同的种子,产生的随机数是相同的);
Random r=new Random();
System.out.println(r.nextBoolean());
System.out.print(r.nextInt(50));//随机生成0~50的随机数,不包括50
System.out.println(r.nextInt(20)+30);//随机生成30~50的随机数,不包括50
System.out.println(r.nextBoolean());
System.out.print(r.nextInt(50));//随机生成0~50的随机数,不包括50
System.out.println(r.nextInt(20)+30);//随机生成30~50的随机数,不包括50
ThreadLocalRandom
JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺
ThreadLocalRandom不是直接用new实例化,而是第一次使用其静态方法current()。
ThreadLocalRandom t=ThreadLocalRandom.current();
System.out.println(t.nextInt(50));//随机生成0~50的随机数,不包括50
System.out.println(t.nextInt(30, 50));//随机生成30~50的随机数,不包括50
System.out.println(t.nextInt(50));//随机生成0~50的随机数,不包括50
System.out.println(t.nextInt(30, 50));//随机生成30~50的随机数,不包括50
ThreadLocalRandom
从Math.random()改变到ThreadLocalRandom有如下好处:
不再有从多个线程访问同一个随机数生成器实例的争夺。
取代以前每个随机变量实例化一个随机数生成器实例,可以每个线程实例化一个。
异常处理
什么是异常?
异常是程序经常会出现的,
发现错误的最佳时机是在编译阶段,也就是你试图在运行程序之前。
但是, 在编译期间并不能找到所有的错误,有一些运行时异常在编译期找不到,这些异常往往在运行时才能被发现。
所谓异常就是程序运行时可能出现一些错误,比如试图打开一个根本不存在的文件等,异常处理将会改变程序的控制流程,让程序有机会对错误作出处理。
如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。
在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。
另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
在Java中,把异常理解为是一种能够提高你程序健壮性的机制,它能够让你在编写代码中注意这些问题
如果写代码不会注意这些异常情况,无法成为一位硬核程序员
异常分类
图解Throwable异常
图解异常
Throwable概述
Throwable是 Java 语言中所有错误或异常的超类。
Error和Exception两者都是Java 异常处理的重要⼦类, 各⾃都包含⼤量⼦类。均继承自Throwable 类。
Throwable类是Java语言中所有错误(errors)和异常(exceptions)的父类。
只有继承于Throwable的类或者其子类才能够被抛出,
还有一种方式是带有Java中的@throw注解的类也可以. 抛出。
对Throwable及其子类进行归类分析
在Java规范中,对非受查异常和受查异常的定义是这样的:
| The unchecked exception classes are the run-time exception classes and the error classes.
The checked exception classes are all exception classes other than the unchecked exception classes. That is, the checked exception classes are Throwable and all its subclasses other than RuntimeException and its subclasses and Error and its subclasses.
The checked exception classes are all exception classes other than the unchecked exception classes. That is, the checked exception classes are Throwable and all its subclasses other than RuntimeException and its subclasses and Error and its subclasses.
也就是说,除了 RuntimeException和其子类,以及error和其子类,其它的所有异常都是 checkedException
对Throwable及其子类进行归类分析
Throwable位于异常和错误的最顶层
按照这种逻辑关系,对Throwable及其子类进行归类分析
查看Throwable类中发现它的方法和属性有很多,几个比较常用的方法
//返回抛出异常的详细信息
public string getMessage();
public string getLocalizedMessage();
public string getMessage();
public string getLocalizedMessage();
//返回异常发生时的简要描述
public public String toString()
public public String toString()
//打印异常信息到标准输出流上
public void printStackTrace();
public void printStackTrace(PrintStream s);
public void printStackTrace(PrintWriter s)
public void printStackTrace();
public void printStackTrace(PrintStream s);
public void printStackTrace(PrintWriter s)
//记录栈帧的的当前状态
public synchronized Throwable fillInStackTrace();
public synchronized Throwable fillInStackTrace();
此外,因为Throwable的父类也是Object,所以.常用的方法还有继承其父类的getClass()和 getName()方法。
写Java程序经常会出现两种问题,都用来表示出现了异常情况
Throwable
Error(java.Iang.Error)
什么是Error类?
表⽰系统级的错误
应用程序不会抛出该类对象。
如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
Error类是指java运行时系统的内部错误和资源耗尽错误。
Error是程序无法处理的错误,表示运行应用程序中较严重问题。
大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM (Java虚拟机)出现的问题。
这些错误是不可检查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况
是java 运⾏环境内部错误或者硬件问题, 不能指望程序来处理这样的问题, 除了退出运⾏外别⽆选择, 它是JVM抛出的。
一般发生在严重故障时,它们在java程序处理的范畴之外
比如OutOfMemoryError和StackOverflowError异常的出现会有几种情况
只有Java内存模型中的程序计数器,是不会发生OutOfMemoryError情况的区域
除了程序计数器外,其他区域都是可能发生 OutOfMemoryError 的区域
方法区(Method Area)
方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
虚拟机栈(VM Stack)
如果线程请求的栈深度大于虚拟机栈所允许的深度,将会出现StackOverflowError异常;
如果虚拟机动态扩展无法申请到足够的内存,将出现OutOfMemoryError异常
本地方法栈(Native Method Stack)
本地方法栈和虚拟机栈一样
堆(Heap)
Java堆可以处于物理上不连续,逻辑上连续,就像我们的磁盘空间一样,
如果堆中没有内存 完成实例分配,并且堆无法扩展时,将会抛出OutOfMemoryError。
Exception (java.Iang.Exception)(异常类型)
认识 Exception,什么是Exception
Exception位于java.lang包下,
它是一种顶级接口,继承于Throwable类,Exception的父类是Throwable
Exception类及其子类都是Throwable的组成条件,是程序出现的合理情况。
表⽰程序需要捕捉、需要处理的异常
是由与程序设计的不完善⽽出现的问题, 程序必须处理的问题
程序可以处理的异常,可以捕获且可以恢复。遇到这类异常应尽快处理
Exception 有两种异常,这两种异常都应该去捕获
运行时异常RuntimeException
⼀般是运⾏时异常, 继承⾃RuntimeException
RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
非受检异常( unchecked exception)
对于⾮受检异常来说,
在编写代码时, 不需要显⽰的捕获,
但是如果不捕获, 在运⾏期,如果发⽣异常,就会中断程序的执⾏。
代码原因以及程序员的错误】
这种异常⼀般可以理解为是代码原因导致的。
如果出现RuntimeException,那么一定是程序员的错误.
只要代码写的没问题, 这些异常都是可以避免的,也就不需要显⽰的进⾏处理。
如果要对所有可能发⽣空指针的地⽅做异常处理, 那相当于所有代码都需要做这件事。
RuntimeException举例如:NullPointerException发⽣空指针异常、ArrayIndexOutOfBoundsException数组越界、ClassCastException类型转换异常;
编译时异常/受检异常CheckedException
检查异常CheckedException
如I/O错误导致的IOException、SQLException、FileNotFoundException。
这种异常在IO 操作中⽐较多
以FileNotFoundException为例
当使⽤IO 流处理⼀个⽂件时, 有⼀种特殊情况, 就是⽂件不存在,
所以, 在⽂件处理的接口定义时,它会显⽰抛出FileNotFoundException,
⽬的就是告诉这个⽅法的调⽤者,这个⽅法不保证⼀定可以成功,
是有可能找不到对应的⽂件的,你要明确的对这种情况做特殊处理哦。
所以说, 当我们希望我们的⽅法调⽤者, 明确的处理⼀些特殊情况的时候, 就应该使⽤受检异常。
一般是外部错误,这种异常都发生在编译阶段
Java编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行try catch
该类异常一般包括几个方面
1. 试图在文件尾部读取数据
2. 试图打开一个错误格式的URL
3. 试图根据给定的字符串查找class对象,而这个字符串表示的类并不存在
⼀定要对该异常进⾏处理( 捕获或者向上抛出),否则是⽆法编译通过的
对于受检异常来说,
如果⼀个⽅法在声明的过程中证明了其要有受检异常抛出:
public void test() throw new Exception{ }
那么,当在程序中调⽤他时, ⼀定要对该异常进⾏处理( 捕获或者向上抛出)
否则是⽆法编译通过的。
这是⼀种强制规范。
举例:如I/O错误导致的IOException、SQLException、FileNotFoundException。
异常对象可以调用如下方法,得到或输出有关异常的信息
public String getMessage();
public void printStackTrace();
public String toString();
public void printStackTrace();
public String toString();
与Exception异常有关的关键字/方法/用法
throws 和 throw
在Java中,异常也就是一个对象,
它能够被程序员自定义抛出或者应用程序抛出,必须借助于throws和throw语句来定义抛出异常。
throws和throw通常是成对出现的
throw语句
用在方法体内,
表示抛出异常,
由方法体内的语句处理。
具体向外抛异常的动作,所以它是抛出一个异常实例。
⽤来明确地抛出⼀个异常
throws语句
用在方法声明后面,
表示再抛出异常
由该方法的调用者来处理。
主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常。
⽤来声明⼀个⽅法可能抛出的各种异常
Throw和throws的区别
区别
【位置不同】
throws用在函数上,后面跟的是异常类,可以跟多个
throw用在函数内,后面跟的是异常对象。
【功能不同】
throws用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;
throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。
也就是说throw语句独立存在时,下面不要定义其他语句,因为执行不到。
throws表示出现异常的一种可能性,并不一定会发生这些异常;
throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
try、finally、catch
这三个关键字主要有下面几种组合方式
try
⽤来指定⼀块预防所有异常的程序;
catch
catch⼦句紧跟在try 块后⾯, ⽤来指定你想要捕获的异常的类型;
finally
为确保⼀段代码不管发⽣什么异常状况都要被执⾏;
try...catch
try~catch语句:表示对某一段代码可能抛出异常进行的捕获
Java使用try~catch语句来处理异常,将可能出现的异常操作放在try~catch语句的try部分,将发生异常后的处理放在catch部分。
try~catch语句的格式如下
try {
包含可能发生异常的语句
} catch(ExceptionSubClass1 e) {
… …
} catch(ExceptionSubClass2 e) {
… …
}
包含可能发生异常的语句
} catch(ExceptionSubClass1 e) {
… …
} catch(ExceptionSubClass2 e) {
… …
}
try...finally
表示对一段代码不管执行情况如何, 都会走finally中的代码
try...catch..fiinally
表示对异常捕获后,再走finally中的代码逻辑
try-with-resources
糖块十一、try-with-resource
Java7以前的流操作
Java 里,对于文件操作IO 流、数据库连接等开销非常昂贵的资源
用完之后必须及时通过close 方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。
关闭资源的常用方式就是在finally 块里是释放,即调用close 方法
比如,经常会写这样的代码:
关闭资源的常用方式就是在finally 块里是释放,即调用close 方法
static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}
Java 7 开始的流操作
从Java 7 开始,jdk 提供了一种更好的方式关闭资源,使用try-with-resources 语句,
改写一下上面的代码,效果如下:
看,这简直是一大福音啊,
static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
try( BufferedReader br = new BufferedReader(new FileReader(path)) ) {
return br.readLine();
}
}
try( BufferedReader br = new BufferedReader(new FileReader(path)) ) {
return br.readLine();
}
}
虽然我之前一般使用IOUtils 去关闭流,并不会使用在finally 中写很多代码的方式,
但是这种新的语法糖看上去好像优雅很多呢。
编译之后
编译之后
其实背后的原理也很简单,那些没有做的关闭资源的操作,编译器都帮我们做了。
所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。
finally 和return 的执行顺序
try() ⾥⾯有⼀个return 语句, 那么后⾯的finally{}⾥⾯的code 会不会被执⾏, 什么时候执⾏, 是在return 前还是return 后?
如果try 中有return 语句, 那么finally 中的代码还是会执⾏。
因为return 表⽰的是要整个⽅法体返回, 所以,finally 中的语句会在return 之前执⾏。
但是return 前执行的finally 块内,对数据的修改效果对于引用类型和值类型会不同:
子主题
请说明final,finally,finalize的区别?
无论是否抛出异常,finally代码块都会执行,它主要是用来释放应用占用的资源。
final关键字
用于声明属性,方法和类,分别表示属性不可变,方法不可重写,类不可继承
finally代码块
异常处理语句结构的一部分,代表总执行
finalize()方法
Object类的一个方法
finalize()方法是Object类的一个protected方法
在垃圾收集器执行时,会调用被回收对象的此方法
它是在对象被垃圾回收之前由Java虚拟机来调用的。
异常的处理方式
遇到问题不进行具体处理,而是继续抛给调用者 (throw,throws)
异常,千万不能捕获了之后什么也不做。或者只是使⽤e.printStacktrace。
抛出异常有三种形式
一是throw
一个throws
还有一种系统自动抛异常
正确处理异常(具体的处理⽅式的选择其实原则⽐较简明)
异常的处理⽅式有两种
1、⾃⼰处理
自己明确的知道如何处理的,就要处理掉。
2、向上抛,交给调⽤者处理。
不知道如何处理的,就交给调⽤者处理。
自定义异常类
也可以扩展Exception类定义自己的异常类,然后规定哪些方法产生这样的异常。
一个方法在声明时可以使用throws关键字声明要产生的若干个异常,并在该方法的方法体中具体给出产生异常的操作,
即用相应的异常类创建对象,并使用throw关键字抛出该异常对象,导致该方法结束执行。
⾃定义异常就是开发人员⾃⼰定义的异常, ⼀般通过继承Exception 的⼦类的⽅式实现。
编写⾃定义异常类实际上是继承⼀个API 标准异常类,⽤新定义的异常处理信息覆盖原有信息的过程。
这种⽤法在Web 开发中也⽐较常见,一般可以⽤来⾃定义业务异常。如余额不⾜、重复提交等。
这种⾃定义异常有业务含义,更容易让上层理解和处理
异常链
“异常链”是什么?
“异常链”是Java 中⾮常流⾏的异常处理概念
是指在进⾏⼀个异常处理时抛出了另外⼀个异常, 由此产⽣了⼀个异常链条。
该技术⼤多⽤于将“ 受检查异常” 封装成为“⾮受检查异常”或者RuntimeException。
如果因为异常你决定抛出⼀个新的异常,⼀定要包含原有的异常,这样, 处理程序才可以通过getCause()和initCause()⽅法来访问异常最终的根源。
从Java 1.4 版本开始,几乎所有的异常都支持异常链。
以下是Throwable 中支持异常链的方法和构造函数。
Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)
initCause 和Throwable 构造函数的Throwable 参数是导致当前异常的异常。
getCause 返回导致当前异常的异常,initCause 设置当前异常的原因。
getCause 返回导致当前异常的异常,initCause 设置当前异常的原因。
以下示例,显示如何使用异常链。
try {
} catch (IOException e) {
throw new SampleException("Other IOException", e);
}
} catch (IOException e) {
throw new SampleException("Other IOException", e);
}
在此示例中,当捕获到IOException 时,将创建一个新的SampleException 异常,
并附加原始的异常原因,并将异常链抛出到下一个更高级别的异常处理程序。
常见问题
生产环境上的OOM?
如何优化try catch
什么是Throwable ?
Java如何进行异常处理?
每当一个方法出现异常后,便抛出一个异常对象,该对象中包含有异常信息。
调用这个对象中的方法,可以捕获这个异常,并进行处理。
Java的异常有几种?Exception和Error的区别是什么?
如何避免NLP异常?
optional
Java中的两种异常类型是什么?他们有什么区别?
受检查的(checked)异常
受检查的异常必须要用throws语句在方法或者是构造函数上声明。
不受检查的(unchecked)异常
不受检查的异常不需要在方法或者是构造函数上声明
就算方法或者是构造函数的执行可能会抛出这样的异常,并且不受检查的异常可以传播到方法或者是构造函数的外面。
运行时异常和一般异常有什么区别?
运行时异常不是必须要catch;一般异常必须要catch
Java中Exception和Error有什么区别?
Exception和Error都是Throwable的子类。
Exception用于用户程序可以捕获的异常情况。
Error定义了不期望被用户程序捕获的异常。
throw和throws有什么区别?
throw关键字用来在程序中明确的抛出异常,相反,throws语句用来表明方法不能处理的异常。
每一个方法都必须要指定哪些异常不能处理,所以方法的调用者才能够确保处理可能发生的异常,多个异常是用逗号分隔的。
Java异常处理的一些小建议。
异常处理的时候,finally代码块的重要性是什么?
无论是否抛出异常,finally代码块总是会被执行。就算是没有catch语句同时又抛出异常的情况下,finally代码块仍然会被执行。
最后要说的是,finally代码块主要用来释放资源,比如:I/O缓冲区,数据库连接。
异常处理完成以后,Exception对象会发生什么变化?
Exception对象会在下一个垃圾回收过程中被回收掉。
注解
图解
图解
Java注解(Annotation)概念
又称为元数据
一种对元程序中元素关联信息和元数据的途径和方法。
元数据(metadata)
一个接口,程序可以通过反射来获取指定程序中元素的Annotation对象,然后通过该Annotation对象来获取注解中的元数据信息。
Annotation(注解)
它为我们在代码中添加信息提供了一种形式化的方法。
Java提供的
它是JDK1.5引入的
Java定义了一套注解
4种标准元注解
@Target
修饰的对象范围
说明了Annotation所修饰的对象范围
在Annotation类型的声明中使用了target可更加明晰其修饰的目标
Annotation可被用于
packages
types(类、接口、枚举、Annotation类型)
类型成员(方法、构造方法、成员变量、枚举值)
方法参数和本地变量(如循环变量、catch参数)
@Retention
定义 被保留的时间长短
定义了该Annotation被保留的时间长短
表示需要在什么级别保存注解信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效),
取值(RetentionPoicy)由:
SOURCE:在源文件中有效(即源文件保留)
CLASS:在class文件中有效(即class保留)
RUNTIME:在运行时有效(即运行时保留)
@Documented
描述-javadoc
用于描述其它类型的annotation应该被作为被标注的程序成员的公共API
可以被例如javadoc此类的工具文档化。
@Inherited
阐述了某个被标注的类型是被继承的
该元注解是一个标记注解
阐述了某个被标注的类型是被继承的
如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
元注解的作用
元注解的作用是负责注解其他注解。
Java5.0定义了4个标准的meta-annotation类型
它们被用来提供对其它 annotation类型作说明。
7个Java注解(Annotation)
3个自定义注解
在java.lang中
作用在代码中的注解
开发中日常使用注解大部分是用在类上,方法上,字段上
作用在代码中的注解,有三个自定义注解
@Override
重写标记,一般用在子类继承父类后,标注在重写过后的子类方法上。
如果发现 其父类,或者是引用的接口中并没有该方法时,会报编译错误。
不是一个元注解。
表示当前方法覆盖了父类的方法。
@Deprecated
用此注解注释的代码已经过时,不再推荐使用
表示方法已经过时,方法上有横线,使用时会有警告。
@SuppressWarnings
这个注解起到忽略编译器的警告作用
表示关闭一些警告信息(通知java 编译器,忽略特定的编译警告)。
自定义注解
除了元注解, 都是自定义注解
通过元注解定义出来的注解。
如常用的Override 、Autowire 等。
日常开发中也可以自定义一个注解,这些都是自定义注解。
剩下4个元注解
在 java.lang.annotation 中。
元注解
用来标志注解的注解
定义其他注解的注解
比如Override 这个注解,就不是一个元注解。而是通过元注解定义出来的。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这里面的@Target @Retention 就是元注解。
元注解的使用,示列代码如下
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableAuth {
}
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableAuth {
}
元注解有四个
@Retention
表示什么级别保存该注解信息
用于指定被修饰的注解被保留多长时间
标识如何存储,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
只有注解的@Retension标注为RUNTIME时, 才能够通过反射获取到该注解
@Retension有3种保存策略
RetentionPolicy.SOURCE
注解只保留在源文件,仅存在于源码中,在class 字节码文件中不包含
只在源文件(.java) 中保存, 即该注解只会保留在源文件中
当Java文件编译成class文件时,注解被遗弃
编译时编译器会忽略该注解
例如,@Override注解
RetentionPolicy.CLASS
默认的保留策略
注解被保留到class文件,但jvm加载class文件时被遗弃,这是默认的生命周期
保存在字节码文件.class中, 注解会随着编译跟随字节码文件中, 但是运行时不会对该注解进行解析
注解会在class 字节码文件中存在,但运行时无法获取
RetentionPolicy.RUNTIME
用得最多
注解,不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
一直保存到运行时, 用得最多的一种保存策略, 在运行时,可以获取到该注解的所有信息
注解会在class 字节码文件中存在,在运行时,可以通过反射获取到
如果想要在程序运行过程中,通过反射来获取注解的信息,需要将Retention 设置为RUNTIME。
举例
像下面这个例子, Small Pineapple类继承了抽象类Pineapple, getInfo()方法上标识有@Override注解, 且在子类中标注了@Transient注解,
在运行时,获取子类重写方法上的所有注解,只能获取到@Transient的信息,
Small Pineapple类继承了抽象类Pineapple, getInfo()方法上标识有@Override注解, 且在子类中标注了@Transient注解,
启动类Bootstrap获取Small Pineapple类中的getInfo0方法上的注解信息:
启动类Bootstrap获取Small Pineapple类中的getInfo0方法上的注解信息:
@Documented
标记这些注解是否包含在JavaDoc中
将此注解包含再JavaDoc中)
用于指定被修饰的注解类将被javadoc 工具提取成文档。
@Target
标记这个注解说明了 Annotation所修饰的对象范围,
用于指定被修饰的注解修饰哪些程序单元,也就是上面说的类,方法,字段。
表示该注解可以用于什么地方
Annotation可被用于
packages
types (类、接口、枚举、Annotation类型)
类型成员(方法、构造方法、成员变 量、枚举值)
方法参数和本地变量(如循环变量、catch参数)
取值如下
@Target取值
@Inherited
标记这个注解是继承于哪个注解类的。
允许子类继承父类中的注解
用于指定被修饰的注解类将具有继承性。
从JDK1.7开始,又添加了三个额外的注解
@SafeVarargs
jdk1.7 更新
在声明可变参数的构造函数或方法时,Java编译器会报unchecked警告。
使用@SafeVarargs可以.忽略这些警告
表示:专门为抑制“堆污染”警告提供的。
@Functionallnterface
jdk1.8 更新
表明这个方法是一个函数式接口
用来指定某个接口必须是函数式接口,否则就会编译出错。
@Repeatable
标识某注解可以在同一个声明上使用多次
注解与反射的结合,通过反射获取注解
注解和反射经常结合在一起使用,在很多框架的代码中都能看到他们结合使用的影子。
获取注解单独拧了出来, 因为它并不是专属于Class对象的一种信息
每个变量, 方法和构造器都可以被注解修饰
只有注解的@Retension标注为RUNTIME时, 才能够通过反射获取到该注解
在反射中, Field, Constructor和Method类对象,都可以调用下面这些方法,获取标注在它们之上的注解。
Annotation[] getAnnotations0
获取该对象上的所有注解
Annotation getAnnotation(Class annotationClass)
传入注解类型, 获取该对象上的特定一个注解
Annotation[] getDeclaredAnnotations()
获取该对象上的显式标注的所有注解, 无法获取继承下来的注解
Annotation getDeclaredAnnotation(Class annotationClass)
根据注解类型, 获取该对象上的特定一个注解,无法获取继承下来的注解
可以通过反射来判断类,方法,字段上是否有某个注解以及获取注解中的值,获取某个类中方法上的注解代码示例如下:
获取某个类中方法上的注解代码示例如下:
通过isAnnotationPresent 判断是否存在某个注解,
通过getAnnotation 获取注解对象,然后获取值。
示例代码
示例参考链接:
https://blog.csdn.net/KKALL1314/article/details/96481557
实现功能如下:
一个类的某些字段上被注解标识,在读取该属性时,将注解中的默认值赋给这些属性,没有标记的属性不赋值:
定义注解接口类
子主题
定义一个类
这里给str1 加了注解,并利用反射解析并赋值:
测试类
测试类
子主题
运行结果:
Person(stra=有注解, strb=无注解, strc=无注解)
APT
当开发者使用了Annotation 修饰了类、方法、Field 等成员之后,
这些Annotation不会自己生效,必须由开发者提供相应的代码来提取并处理Annotation 信息。
这些处理提取和处理Annotation 的代码,统称为APT(Annotation Processing Tool)。
注解的提取需要借助于Java 的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。
如何自定义一个注解?
使用@interface定义的注解类
在Java 中,类使用class 定义,接口使用interface 定义,
注解和接口的定义差不多,增加了一个@符号,即@interface
public @interface EnableAuth {
}
}
定义成员变量,用于信息的描述
注解中可以定义成员变量,用于信息的描述,
跟接口中方法的定义类似,
定义成员变量,用于信息的描述
可以添加默认值
可以添加默认值
注解处理器
如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。
使用注解的过程中,很重要的一部分就是创建于使用注解处理器。
Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。下面实现一个注解处理器。
反射
诞生背景
假如需要实例化一个HashMap,
【第一版本】代码就会是这样子,
实例化一个HashMap
【第二版本】某一天发现, 该段程序不适合用HashMap存储键值对, 更倾向于用Linked HashMap存储。
重新编写代码后变成下面这个样子。
更倾向于用Linked HashMap存储
【第三版本】假如又有一天, 发现数据还是适合用HashMap来存储, 难道又要重新修改源码吗?
发现问题了吗? 每次改变一种需求,都要去重新修改源码,然后对代码进行编译,打包,再到JVM上重启项目, 这么些步骤下来, 效率非常低。
图解
图解
【第N版本】对于这种需求频繁变更但变更不大的场景,频繁地更改源码肯定是一种不允许的操作
可以使用一个开关,判断什么时候使用哪一种数据结构.
使用一个开关,判断什么时候使用哪一种数据结构.
通过传入参数param决定使用哪一种数据结构, 可以在项目运行时, 通过动态传入参数决定使用哪一个数据结构。
【第N+1版本】如果某一天还想用TreeMap, 还是避免不了修改源码, 重新编译执行的弊端。这个时候, 反射就派上用场了。
在代码运行之前,不确定将来会使用哪一种数据结构,只有在程序运行时才决定使用哪一个数据类,而反射可以在程序运行过程中动态获取类信息和调用类方法。
通过反射构造类实例,代码会演变成下面这样。
通过反射构造类实例
回顾一下如何从new一个对象引出使用反射的
在不使用反射时, 构造对象使用new方式实现, 这种方式在编译期就可以把对象的类型确定下来。
如果需求发生变更,需要构造另一个对象,则需要修改源码,非常不优雅,
所以通过使用开关,在程序运行时判断需要构造哪一个对象,在运行时可以变更开关来实例化不同的数据结构。
如果还有其它扩展的类有可能被使用,就会创建出非常多的分支,且在编码时不知道有什么其他
的类被使用到, 假如日后Map接口下多了一个集合类是xxx HashMap, 还得创建分支, 此时引出
了反射:可以在运行时才确定使用哪一个数据类,在切换类时,无需重新修改源码、编译程序。
的类被使用到, 假如日后Map接口下多了一个集合类是xxx HashMap, 还得创建分支, 此时引出
了反射:可以在运行时才确定使用哪一个数据类,在切换类时,无需重新修改源码、编译程序。
反射机制概念是什么?
Java中一个非常重要的知识点,同时也是一个高级特性
(运行状态中知道类所有的属性和方法)
在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;
并且对于任意一个对象,都能够调用它的任意一个方法;
反射机制指的是程序在运行时能够获取自身的信息。
在java 中,只要给定类的名字,那么就可以通过反射机制来获得类的所有属性和方法。
反射就像是一个掌控全局的角色,不管你程序怎么运行,我都能够知道你这个类有哪些属性 和方法,你这个对象是由谁调用的
反射是开源框架中的一个重要设计理念,在源码分析中少不了它的身影
Java语言的反射机制
在运行过程中,对于任何一个类,都能够知道它的所有属性和方法
【动态获取信息】
在运行过程中,对于任意一个对象,都能够知道调用它的任意属性和方法
【动态调用对象】
反射的思想
在程序运行过程中确定和解析数据类的类型
反射的作用
对于在编译期无法确定使用哪个数据类的场景,通过反射,可以在程序运行时构造出不同的数据类实例
正射是什么
平常用的最多的new方式实例化对象的方式,就是一种正射的体现
使用场景、应用场景
编译时类型和运行时类型
在Java程序中许多对象在运行是都会出现两种类型:编译时类型和运行时类型。
编译时的类型
由声明对象时使用的类型来决定
运行时的类型
由实际赋值给对象的类型决定
如:Person p=new Student(); 其中编译时类型为Person,运行时类型为Student。
编译时类型无法获取具体方法
程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为Object,但是程序有需要调用该对象的运行时类型的方法。
为了解决这些问题,程序需要在运行时发现对象和类的真实信息。
然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,此时就必须使用到反射了。
Spring框架,Bean的初始化用到了反射、Spring实例化对象
Bean的初始化用到了反射、Spring实例化对象
Spring的IOC容器
当程序启动时, Spring会读取配置文件applicationContext.xml
配置文件applicationContext.xml
在Spring中, 经常会编写一个上下文配置文件applicationContext.xml,
在定义好上面的文件后, 通过ClassPathXmlApplicationContext加载该配置文件
通过ClassPathXmlApplicationContext加载该配置文件
IOC容器本质上就是一个工厂,通过该工厂传入<bean>标签的id属性获取到对应的实例.
程序启动时,Spring会将该配置文件中的所有bean都实例化, 放入IOC容器中,
并解析出里面所有的标签,实例化到IOC的容器中
里面就是关于bean的配置, 程序启动时会读取该xml文件, 解析出所有的<bean>标签, 并实例化对象放入IOC容器中。
Spring在实例化对象的过程经过简化之后, 可以理解为反射实例化对象的步骤:
获取Class对象的构造器
通过构造器调用newInstance() 实例化对象
当然,Spring在实例化对象时, 做了非常多额外的操作, 才能够让现在的开发足够的便捷且稳定
基本上Spring等一系列框架,都是基于反射的思想 写成的。
反射+工厂模式消除工厂中的多个分支
通过反射消除工厂中的多个分支,如果需要生产新的类,无需关注工厂类,工厂类可以应对各种新增的类,反射可以使得程序更加健壮
传统的工厂模式,如果需要生产新的子类,需要修改工厂类,在工厂类中增加新的分支:
传统的工厂模式
利用反射和工厂模式相结合,在产生新的子类时,工厂类不用修改任何东西,可以专注于子类的实现,当子类确定下来时,工厂也就可以生产该子类了。
核心思想
在运行时,通过参数传入不同子类的全限定名,获取到不同的Class对象, 调用newInstance() 方法返回不同的子类。
反射+抽象工厂模式,一般会用于有继承或者接口实现关系
举例:在运行时,才确定使用哪一种Map结构, 可以利用反射传入某个具体Map的全限定名, 实例化一个特定的子类。
在运行时,才确定使用哪一种Map结构
className可以指定为java.util.HashMap,或者java.util.TreeMap等等,根据业务场景来定,
JDBC连接数据库、JDBC加载数据库驱动类
使用JDBC连接数据库时, 指定连接数据库的驱动类时用到反射加载驱动类
在导入第三方库时, JVM不会主动去加载外部导入的类,
而是等到真正使用时, 才去加载需要的类, 正是如此, 可以在获取数据库连接时传入驱动类的全限定名, 交给JVM加载该类
案例代码
指定连接数据库的驱动类时,用到反射,加载驱动类
Springboot中的驱动
开发Spring Boot项目时, 会经常遇到这个类, 但是可能习惯成自然了, 就没多大在乎
看看常见的application.yml中的数据库配置, 应该会恍然大悟
application.yml中的数据库配置
这里的driver-class-name, 和一开始加载的类是不是觉得很相似, 这是因为MySQL版本不同引起的驱动类不同,
使用反射的好处:不需要修改源码,仅加载配置文件就可以完成驱动类的替换。
在获取标注的注解时,也会用到反射
在反射中, Field, Constructor和Method类对象,都可以调用下面这些方法,获取标注在它们之上的注解。
Annotation[] getAnnotations0
获取该对象上的所有注解
Annotation getAnnotation(Class annotationClass)
传入注解类型, 获取该对象上的特定一个注解
Annotation[] getDeclaredAnnotations()
获取该对象上的显式标注的所有注解, 无法获取继承下来的注解
Annotation getDeclaredAnnotation(Class annotationClass)
根据注解类型, 获取该对象上的特定一个注解,无法获取继承下来的注解
只有注解的@Retension标注为RUNTIME时, 才能够通过反射获取到该注解
在破坏单例模式时也用到了反射
还记得单例模式一文吗 ? 里面讲到,反射破坏饿汉式和懒汉式单例模式,所以之后用了枚举,避免被反射KO。
回到最初的起点
Small Pineapple里有一个weight属性被private修饰符修饰, 目的在于自己的体重并不想给外界知道。
Small Pineapple里有一个weight属性被private修饰符修饰, 目的在于自己的体重并不想给外界知道。
虽然weight属性理论上只有自己知道, 但是如果经过反射, 这个类就像在裸奔一样, 在反射面前变得一览无遗。
如果经过反射, 这个类就像在裸奔一样, 在反射面前变得一览无遗。
反射主要提供了以下这几个功能
反射中的用法有非常非常多,常见的功能有以下这几个
在运行时
判断任意一个对象所属的类/获取一个类的Class对象
Class 类
Class 类是什么?
在Java中,你每定义一个java class实体都会产生一个Class对象。
当编写一个类, 编译完成后,在生成的.class文件中,就会产生一个Class对象,这个Class对象用于表示这个类 的类型信息。
当一个类或接口被装入的JVM 时便会产生一个与之关联的java.lang.Class 对象,可以通过这个Class 对象对被装入类的详细信息进行访问。
Java.lang.Class 是一个比较特殊的类
Class中没有公共的构造器
Class对象不能被实例化。
任何运行在内存中的所有类都是该Class类的实例对象, 每个Class类对象内部都包含了本来的所有信息。
Java 的Class 类是java 反射机制的基础
通过Class 类,可以获得关于一个类的相关信息。
它用于封装被装入到JVM 中的类(包括类和接口)的信息。
每个类(型)都有一个Class 对象
JVM为每种类型管理一个独一无二的Class 对象
运行程序时,JVM先检查是否所要加载的类对应的Class 对象是否已经加载
如果没有加载,JVM 就会根据类名查找.class 文件,并将其Class 对象载入。
获取Class对象的三种方法
调用某个类的class属性来获取该类对应的Class对象
类名.class
类名.class:
Class clazz=Person.class;
这种获取方式只有在编译前已经声明了该类的类型才能获取到Class对象
调用某个对象的getClass()方法
实例.getClass()
实例.getClass()
Person p=new Person();
Class clazz=p.getClass();
通过实例化对象,获取该实例的Class对象
使用Class类中的forName()静态方法(最安全/性能最好)
Class clazz=Class.forName("类的全路径"); (最常用)
Class.for Name(class Name)
Class.for Name(class Name)
//获取Person类的Class对象
Class clazz=Class.forName("reflection.Person");
Class clazz=Class.forName("reflection.Person");
//获取Person类的所有方法信息
Method[] method=clazz.getDeclaredMethods();
for(Method m:method){
System.out.println(m.toString());
}
Method[] method=clazz.getDeclaredMethods();
for(Method m:method){
System.out.println(m.toString());
}
//获取Person类的所有成员属性信息
Field[] field=clazz.getDeclaredFields();
for(Field f:field){
System.out.println(f.toString());
}
Field[] field=clazz.getDeclaredFields();
for(Field f:field){
System.out.println(f.toString());
}
//获取Person类的所有构造方法信息
Constructor[] constructor=clazz.getDeclaredConstructors();
for(Constructor c:constructor){
System.out.println(c.toString());
}
Constructor[] constructor=clazz.getDeclaredConstructors();
for(Constructor c:constructor){
System.out.println(c.toString());
}
通过类的全限定名获取该类的Class对象
当获得了想要操作的类的Class对象后,可以通过Class类中的方法获取并查看该类中的方法和属性。
常用方法
toString()
toString()方法能够将对象转换为字符串
toString()首先会判断Class类型是否是接口类型
普通类和接口都能够用Class对象来表示,然后再判断是否是基本数据类型,
这里判断的都是基本数据类型和包装类,还有void类型。
所有的类型如下
• java.lang.Boolean :代表boolean数据类型的包装类
• java.Iang.Character :代表char数据类型的包装类
• java.lang.Byte:代表byte数据类型的包装类
• java.lang.Short :代表short数据类型的包装类
• java.lang.Integer :代表int数据类型的包装类
• java.lang.Long :代表long数据类型的包装类
• java.lang.Float :代表float数据类型的包装类
java.lang.Double :代表double数据类型的包装类
• java.lang.Void :代表void数据类型的包装类
• java.Iang.Character :代表char数据类型的包装类
• java.lang.Byte:代表byte数据类型的包装类
• java.lang.Short :代表short数据类型的包装类
• java.lang.Integer :代表int数据类型的包装类
• java.lang.Long :代表long数据类型的包装类
• java.lang.Float :代表float数据类型的包装类
java.lang.Double :代表double数据类型的包装类
• java.lang.Void :代表void数据类型的包装类
getName()
然后是getName()方法,这个方法返回类的全限定名称。
•如果是引用类型,比如 String.class.getName。-> java.lang.String
•如果是基本数据类型,byte.class.getName。-> byte
•如果是数组类型,new Object[3]).getClass().getNameO -> [Ljava.lang.Object
•如果是基本数据类型,byte.class.getName。-> byte
•如果是数组类型,new Object[3]).getClass().getNameO -> [Ljava.lang.Object
toString()源码
子主题
toGenericString()
这个方法会返回类的全限定名称,而且包括类的修饰符和类型参数信息。
forName()
根据类名获得一个Class对象的引用,这个方法会使类对象进行初始化。
例如 Class t = Class.forName("java.lang.Thread")就能够初始化一个 Thread 线程对象
三种获取类实例的方式
Class.forName(java.lang.Thread)
Thread.class
thread.getClass()
newInstance()
创建一个类的实例,代表着这个类的对象。
上面forName()方法对类进行初始化,newInstance()方法对 类进行实例化。
其他更多
getClassLoader()获取类加载器对象。
getTypeParameters()按照声明的顺序获取对象的参数类型信息。
getPackage()返回类的包
getInterfaces()获得当前类实现的类或是接口,可能是有多个,所以返回的是Class数组。
Cast把对象转换成代表类或是接口的对象
asSubclass(Class clazz)把传递的类的对象转换成代表其子类的对象
getClasses()返回一个数组,数组中包含该类中所有公共类和接口类的对象
getDeclaredClasses()返回一个数组,数组中包含该类中所有类和接口类的对象
getSimpleName()获得类的名字
getFields()获得所有公有的属性对象
getField(String name)获得某个公有的属性对象
getDeclaredField(String name)获得某个属性对象
getDeclaredFields()获得所有属性对象
getAnnotation(Class annotationClass)返回该类中与参数类型匹配的公有注解对象
getAnnotations()返回该类所有的公有注解对象
getDeclaredAnnotation(Class annotationClass)返回该类中与参数类型匹配的所有注解对象
getDeclaredAnnotations()返回该类所有的注解对象
getConstructor(Class...<?> parameterTypes)获得该类中与参数类型匹配的公有构造方法
getConstructors()获得该类的所有公有构造方法
getDeclaredConstructor(Class...<?> parameterTypes)获得该类中与参数类型匹配的构造方法
getDeclaredConstructors()获得该类所有构造方法
getMethod(String name, Class...<?> parameterTypes)获得该类某个公有的方法
getMethods()获得该类所有公有的方法
getDeclaredMethod(String name, Class...<?> parameterTypes)获得该类某个方法
getDeclaredMethods()获得该类所有方法
记着一句话, 通过反射干任何事, 先找Class准没错!
拿到Class对象就可以对它为所欲为了
剥开它的皮(获取类信息)
指挥它做事(调用它的方法)
看透它的一切(获取属性)
ClassLoader 类
不过,在程序中, 每个类的Class对象只有一个, 也就是说,只有这一个奴隶。
反射中,还有一个非常重要的类就是ClassLoader类,类装载器是用来把类(class)装载进JVM 的。
ClassLoader使用的是双亲委托模型来搜索加载类的,这个模型也就是双亲委派模型。
ClassLoader的类继承图如下
ClassLoader的类继承图
内存中只有一个Class对象的原因:JVM类加载机制的双亲委派模型
它保证了程序运行时, 加载类时每个类在内存中仅会产生一个Class对象。
JVM帮我们保证了一个类在内存中至多存在一个Class对象。
代码讲解
讲解的代码
用上面三种方式测试, 通过三种方式打印各个Class对象都是相同的。
在运行时
构造一个类的实例化对象/构造任意一个类的对象
构造一个类的实例方式有2种 、创建对象的两种方法
Class对象调用newInstance()方法
即使Small Pineapple已经显式定义了构造方法,
通过newInstance() 创建的实例中, 所有属性值都是对应类型的初始值, 因为newinstance()构造实例会调用默认无参构造器.
使用Class对象的newInstance()方法来创建该Class对象对应类的实例,但是这种方法要求该Class对象对应的类有默认的空构造器。
//获取Person类的Class对象 Class clazz=Class.forName("reflection.Person");
//使用.newInstane方法创建对象 Person p=(Person) clazz.newInstance();
Constructor构造器/Constructor对象调用newInstance() 方法
通过getConstructor(Object..param Types) 方法,指定获取指定参数类型的Constructor
Constuctor调用newInstance(Object...param Values) 时,传入构造方法参数的值, 同样可以构造一个实例, 且内部属性已经被赋值
通过Class对象调用newInstance(),会走默认无参构造方法
如果想通过显式构造方法构造实例, 需要提前从Class中调用getConstructor()方法获取对应的构造器, 通过构造器去实例化对象。
先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建 Class对象对应类的实例
,通过这种方法可以选定构造方法创建实例。
//获取Person类的Class对象 Class clazz=Class.forName("reflection.Person");
//获取构造方法并创建对象 Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
//创建对象并设置属性
Person p1=(Person) c.newInstance("李四","男",20);
在运行时
调用任意一个对象的方法/判断任意一个类所有的成员变量和方法/获取一个类的所有信息
Class对象中包含了该类的所有信息
类的所有信息包括:变量、方法、构造器、注解
在编译期,能看到的信息就是该类的变量、方法、构造器
在运行时,最常被获取的也是这些信息。
Class类内部主要信息
Field类(变量)
是什么?
Field类提供类或接口中单独字段的信息,以及对单独字段的动态访问。
描述一个类的属性, 内部包含了该属性的所有信息, 例如数据类型, 属性名, 访问修饰符
常用方法
equals(Object obj) 属性与obj相等则返回true
equals(Object obj) 属性与obj相等则返回true
get(ObJect obJ)获得obj中对应的属性值
set(ObJect obJ, ObJect value)设置obj中对应属性值
包括public和非public修饰的变量,且可获取继承下来的变量
注意:获取不了父类的被protected修饰的变量
获取类中的变量(Field)
获取类中的变量(Field)
Constructor类(构造器)
描述一个类的构造方法, 内部包含了构造方法的所有信息, 例如参数类型, 参数名字,访问修饰符
包括public和非public修饰的构造器
可以通过setAccessible()方法强制访问该构造器
实例化对象,这是破坏懒汉式和饿汉式单例模式的途径
获取类的构造器Constructor
子主题
Method类(普通方法)
是什么?
描述一个类的所有方法(包括抽象方法) , 内部包含了该方法的所有信息,
与Constructor类似, 不同之处是Method拥有返回值类型信息, 因为构造方法是没有返回值的。
包括public和非public修饰的方法,且可获取继承下来的方法
注意:无法获取父类的被protected关键字修饰的方法
invoke(ObJect obJ, ObJect... args)传递object对象及参数调用该对象对应的方法
获取类中的方法(Method)
获取类中的方法(Method)
每种功能内部以Declared细分为2类
有Declared修饰的方法
可以获取该类内部包含的所有变量、方法和构造器
但是无法获取继承下来的信息
无Declared修饰的方法
可以获取该类中public修饰的变量、方法和构造器
可获取继承下来的信息
如果想获取类中所有的(包括继承) 变量、方法和构造器, 则需要同时调用getXXXs() 和 getDeclaredXXXs() 两个方法,
用Set集合存储它们获得的变量、构造器和方法, 以防两个方法获取到相同的东西.
例如:要获取Small Pineapple获取类中所有的变量, 代码应该是下面这样写。
例如:要获取Small Pineapple获取类中所有的变量, 代码应该是下面这样写。
通过反射调用方法
通过反射获取到某个Method类对象后, 可以通过调用invoke方法执行,
invoke(Oject obj, Object...args)
参数1:指定调用该方法的对象
参数2:方法的参数列表值
如果调用的方法是静态方法, 参数1只需要传入null, 因为静态方法不与某个对象有关, 只与某个类有关。
代码举例
可以像下面这种做法, 通过反射实例化一个对象, 然后获取Method方法对象, 调用invoke()指定Small Pineapple的getInfo()方法。
通过反射实例化一个对象, 然后获取Method方法对象, 调用invoke()指定Small Pineapple的getInfo()方法。
反射的优势及缺陷
优点
增加程序的灵活性:面对需求变更时,可以灵活地实例化不同对象
举例
利用反射连接数据库, 涉及到数据库的数据源。
在Spring Boot中一切约定大于配置, 想要定制配置时, 使用application.properties配置文件指定数据源
角色1-Java的设计者:
设计好DataSource接口, 其它数据库厂商想要开发者用各自数据源监控数据库,就得实现我的这个接口!
角色2-数据库厂商
MySQL数据库厂商
提供了com.mysql.cj.jdbc.My sqlDataSource数据源, 开发者可以使用它连接MySQL。
阿里巴巴厂商
提供了com.alibaba.druid.pool.Druid DataSource数据源, 这个数据源更牛逼, 具有页面监控, 慢SQL日志记录等功能, 开发者快来用它监控MySQL吧!
SQLServer厂商
提供了com.microsoft.sqlserver.jdbc.SQLServer DataSource数据源,如果想使用用SQLServer作为数据库, 那就使用我们的这个数据源连接吧
角色3-开发者
可以用配置文件指定使用Druid DataSource数据源
使用Druid DataSource数据源
需求变更:某一天, 老板来说, Druid数据源不太符合我们现在的项目了, 使用MysqlDataSource吧, 然后程序员就会修改配置文件, 重新加载配置文件, 并重启项目, 完成数据源的切换
使用MysqlDataSource
在改变连接数据库的数据源时,只需要改变配置文件即可,无需改变任何代码
原因是:Spring Boot底层封装好了连接数据库的数据源配置
利用反射, 适配各个数据源。
源码分析
用ctrl+左键点击spring.datasource.type进入DataSource Properties类中
发现使用setType0将全类名转化为Class对象注入到type成员变量当中。在连接并监控数据库时,就会使用指定的数据源操作。
DataSource Properties类源码
Class对象指定了泛型上界DataSource, 我们去看一下各大数据源的类图结构。
各大数据源的类图结构
上图展示了一部分数据源,当然不止这些,可以看到,无论指定使用哪一种数据源,都只需要与配置文件打交道,而无需更改源码,这就是反射的灵活性!
缺点
破坏类的封装性
破坏类的封装性
可以强制访问private修饰的信息
很明显的一个特点, 反射可以获取类中被private修饰的变量、方法和构造器, 这违反了面向对象的封装特性
因为被private修饰意味着不想对外暴露, 只允许本类访问, 而setAccessable(true) 可以无视访问修饰符的限制,外界可以强制访问。
性能损耗
性能损耗
反射相比直接实例化对象、调用方法、访问变量,中间需要非常多的检查步骤和解析步骤, JVM无法对它们优化
直接new对象,并调用对象方法和访问属性时
编译器会在编译期提前检查可访问性
如果尝试进行不正确的访问, IDE会提前提示错误
例如,参数传递类型不匹配, 非法访问private属性和方法。
利用反射操作对象时
编译器无法提前得知对象的类型,访问是否合法,参数传递类型是否匹配。
只有在程序运行时,调用反射的代码时,才会从头开始检查、调用、返回结果, JVM也无法对反射的代码进行优化。
虽然反射具有性能损耗的特点,但是我们不能一概而论,产生了使用反射就会性能下降的思想
反射的慢,需要同时调用上100W次才可能体现出来,在几次、几十次的调用,并不能体现反射的性能低下
不要一味地戴有色眼镜看反射,在单次调用反射的过程中,性能损耗可以忽路不计。
如果程序的性能要求很高,那么尽量不要使用反射。
动态语言
指程序在运行时可以改变其结构
新的函数可以引进,已有的函数可以被删除等结构上的变化
动态语言
JavaScript
Ruby
Python
不属于动态语言
C
C++
从反射角度说,JAVA属于半动态语言。
图解反射
图解反射
Java反射API
反射API用来生成JVM中的类、接口或则对象的信息。
1. Class类:反射的核心类,可以获取类的属性,方法等信息。
2. Field类:Java.lang.reflec包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
3. Method类: Java.lang.reflec包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
4. Constructor类: Java.lang.reflec包中的类,表示类的构造方法。
反射使用步骤(获取Class对象、调用对象方法)
1. 获取想要操作的类的Class对象,他是反射的核心,通过Class对象我们可以任意调用类的方法。
2. 调用Class类中的方法,既就是反射的使用阶段。
3. 使用反射API来操作这些信息。
反射的基本使用,与Java反射有关的类,Java反射的主要组成部分有4个
在Java中,使用Java.lang.reflect包实现了反射机制。
Java.lang.reflect所设计的类如下
Java.lang.reflect
与Java反射有关的类
如果父类的属性用protected修饰, 利用反射是无法获取到的。
protected修饰符的作用范围:只允许同一个包下或者子类访问, 可以维承到子类。
getFields()只能获取到本类的public属性的变量值:
getDeclaredFields()只能获取到本类的所有属性, 不包括继承的;
无论如何,都获取不到父类的protected属性修饰的变量, 但是它的的确确存在于子类中
常见问题
你用过反射吗?
说说你知道的反射机制?
如何使用反射调用是有方法?
invoke?
什么是反射?
编码方式
两大字符集
ASCII
什么是ASCII?
ASCII( American Standard Code for InformationInterchange,美国信息交换标准代码),
是基于拉丁字母的⼀套电脑编码系统
主要⽤于显⽰现代英语和其他西欧语⾔。
现今最通⽤的单字节编码系统,并等同于国际标准ISO/IEC646
什么是标准ASCII 码 ?
也叫基础ASCII 码
使⽤7位⼆进制数( 剩下的1 位⼆进制为0)来表⽰所有的⼤写和⼩写字母
数字0 到9、标点符号,以及在美式英语中使⽤的特殊控制字符。
其中:
0~31 及127(共33 个)是
ASCII 值为8、9、10 和13
分别转换为退格、制表、换⾏和回车字符。
它们并没有特定的图形显⽰, 但会依不同的应⽤程序,⽽对⽂本显⽰有不同的影响:
控制字符
LF( 换⾏)
CR( 回车)
FF( 换页)
DEL( 删除)
BS( 退格)
BEL( 响铃)等
通信专⽤字符
SOH( ⽂头)
EOT( ⽂尾)
ACK(确认)等
32~126(共95 个)是字符(32 是空格)
其余为可显⽰字符
其中48~57 为0 到9⼗个阿拉伯数字。
65~90 为26 个⼤写英⽂字母
97~122 号为26 个⼩写英⽂字母
其余为⼀些标点符号、运算符号等。
ASCII 码只能满足美国人的使用
ASCII 码,只有256 个字符,
美国人倒是没啥问题了,他们用到的字符几乎都包括了,
但是世界上不只有美国程序员啊,所以需要一种更加全面的字符集。
Unicode
什么是Unicode?
Unicode(中文:万国码、国际码、统一码、单一码)
是计算机科学领域里的一项业界标准。
它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。
Unicode 是容纳世界所有文字符号的国际标准编码,使用四个字节为每个字符编码。
unicode 虽然统一了全世界字符的二进制编码,但没有规定如何存储
广义的Unicode是一个标准,定义了
一个字符集
一系列的编码规则
Unicode 可以表示中文
Unicode被广泛应用于计算机软件领域
Unicode 伴随着通用字符集的标准而发展,同时也以书本的形式对外发表。
Unicode 备受认可,并广泛地应用于计算机软件的国际化与本地化过程
有很多新科技,都采用Unicode 编码
可扩展置标语言(简称:XML)
Java 编程语言
现代的操作系统
Unicode至今仍在不断增修
Unicode至今仍在不断增修,每个新版本都加入更多新的字符。
目前最新的版本为2018 年6 月5日公布的11.0.0,已经收录超过13 万个字符。
(第十万个字符在2005 年获采纳)
Unicode 发展由非营利机构统一码联盟负责
该机构致力于
让Unicode 方案取代既有的字符编码方案。
因为既有的方案往往空间非常有限,亦不适用于多语环境。
Unicode 涵盖的数据
视觉上的字形
编码方法
标准的字符编码
字符特性,如大小写字母。
子主题
有了Unicode 为啥还需要UTF-8?
广义的Unicode是一个标准,定义了
一个字符集
即Unicode 字符集
一系列的编码规则
UTF-8、
UTF-16、
UTF-32
有了Unicode 为啥还需要UTF-8?
Unicode 是字符集
UTF-8 是编码规则
Unicode 的多种存储方式的诞生背景
unicode 虽然统一了全世界字符的二进制编码,但没有规定如何存储
如果Unicode 统一规定,每个符号就要用三个或四个字节表示,因为字符太多,只能用这么多字节才能表示完全。
一旦这么规定,那么每个英文字母前都必然有二到三个字节是0,
因为所有英文字母在ASCII 中都有,都可以用一个字节表示,剩余字节位置就要补充0。
如果这样,文本文件的大小会因此大出二三倍,这对于存储来说是极大的浪费。
这样导致一个后果:出现了Unicode 的多种存储方式。
UTF 系列编码方案/编码规则
UTF
是什么?
Unicode Transformation Format 的英文缩写
意为把Unicode 字符转换为某种格式。
UTF 系列编码方案
(UTF-8、UTF-16、UTF-32)
均是由Unicode编码方案衍变而来,以适应不同的数据存储或传递
它们都可以完全表示Unicode 标准中的所有字符
目前,这些衍变方案中,UTF-8 被广泛使用,而UTF-16 和UTF-32则很少被使用。
UTF-8
UTF-8是什么?
通过他的英文名Unicode Tranformation Format 就可以知道
UTF-8 就是Unicode 的一个使用方式
使用可变长度(1至4个)字节来储存Unicode 字符,为每个字符编码
使用1 字节储存
ASCII 字母
使用2 字节来储存
重音文字
希腊字母
西里尔字母
使用3 字节来储存
常用的汉字
大部分汉字
使用4 字节来储存
少量不常用汉字
辅助平面字符
UTF-8被广泛应用的原因
因为UTF-8 是可变长度的编码方式,相对于Unicode编码可以减少存储占用的空间
因为使用这种编码方式可以大大节省空间。
比如纯英文网站,就要比纯中文网站,占用的存储小一些。
一般情况下,同一个地区只会出现一种文字类型,
比如中文地区,一般很少出现韩文,日文等。
UTF-16
使用二或四个字节为每个字符编码,其中
大部分汉字采用两个字节编码
少量不常用汉字采用四个字节编码
编码有大尾序和小尾序之别
UTF-16BE
UTF-16BE 以FEFF代表
UTF-16LE
UTF-16LE 以FFFE 代表
在编码前,会放置一个 或
U+FEFF
其中U+FEFF 字符在Unicode 中代表的意义是ZERO WIDTH NO-BREAK SPACE
顾名思义,它是个没有宽度,也没有断字的空白。
U+FFFE
UTF-32
使用四个字节为每个字符编码,使得UTF-32 占用空间,通常会是其它编码的二到四倍。
与UTF-16 一样,有大尾序和小尾序之别,编码前会放置两个内容,以区分。
U+0000FEFF
U+0000FFFE
有了UTF8 为什么还需要GBK?
其实UTF8 确实已经是国际通用的字符编码了,但是这种字符标准毕竟是外国定的,
而国内也有类似的标准指定组织,也需要制定一套国内通用的标准,于是GBK 就诞生了。
GBK系列的编码方式
GB2312(1980 年)
16 位字符集,收录有6763 个简体汉字,682 个符号,共7445 个字符;
优点
适用于简体中文环境,
属于中国国家标准,通行于大陆,新加坡等地也使用此编码;
缺点
不兼容繁体中文,其汉字集合过少。
GBK(1995 年)
16 位字符集,收录有21003 个汉字,883 个符号,共21886 个字符;
优点
最常用的
适用于简繁中文共存的环境,为简体Windows 所使用(代码页cp936)
向下完全兼容gb2312,向上支持ISO-10646 国际标准;
所有字符都可以一对一映射到unicode2.0 上;
缺点
不属于官方标准,和big5 之间需要转换;
很多搜索引擎都不能很好地支持GBK 汉字。
GB18030(2000 年)
32 位字符集;收录了27484 个汉字,同时收录了藏文、蒙文、维吾尔文等主要的少数民族文字
优点
可以收录所有你能想到的文字和符号,属于中国最新的国家标准;
缺点
目前支持它的软件较少
URL 编解码
为什么需要编码
网络标准RFC1738 做了硬性规定
只有一些内容,才可以不经过编码,直接用于URL
字母
数字[0-9a-zA-Z]
一些特殊符号“$-_.+!*'(),”[不包括双引号]
某些保留字
除此以外的字符,是无法在URL 中展示的,所以,遇到这种字符,如中文,就需要进行编码。
URL 编码
把 带有特殊字符的URL 转成可以显示的URL 过程
URL解码
把可以显示的URL 转成带有特殊字符的URL 过程
URL编码可以使用不同的方式, 如
escape
URLEncode
encodeURIComponent
Big Endian大端序 和Little Endian小端序
字节序是什么?
字节顺序
指的是多字节的数据,在内存中的存放顺序
在几乎所有的机器上,多字节对象都被存储为连续的字节序列。
举例
如果C/C++中的一个int 型变量a 的起始地址是&a = 0x100,
那么a 的四个字节,将被存储在存储器的0x100, 0x101, 0x102, 0x103 位置。
根据整数a 在连续的4 byte 内存中的存储顺序,字节序被分为两类
大端序(BigEndian)
指低地址端存放高位字节
Java 采用Big Endian 来存储数据
小端序(Little Endian)
指低地址端存放低位字节。
C\C++采用Little Endian
比如数字0x12345678 ,在两种不同字节序,CPU 中的存储顺序不同
Big Endian:
12345678
Little Endian :
78563412
网络传输一般采用的网络字节序是BIG-ENDIAN,和Java 是一致的
通信的双方都是C/C++
用C/C++写通信程序时
在发送数据前,务必把整型和短整型的数据,进行从主机字节序到网络字节序的转换,
而接收数据后,对于整型和短整型数据,则必须实现从网络字节序到主机字节序的转换
通信的一方是JAVA 程序、一方是C/C++程序时
在C/C++一侧,使用以上几个方法进行字节序的转换
JAVA 一侧,则不需要做任何处理,
因为JAVA 字节序与网络字节序都是BIG-ENDIAN,
只要C/C++一侧能正确进行转换即可
(发送前从主机序到网络序,接收时反变换)
如果通信的双方都是JAVA,
根本不用考虑字节序的问题了。
时间处理
时区与时差
时区是什么?
时区是地球上的区域使用同一个时间定义。
以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同经度的地方的时间有所不同(地方时)
1863 年,首次使用时区的概念。
时区通过设立一个区域的标准时间部分地解决了这个问题。
时差是什么?
世界各个国家位于地球不同位置上,
因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差。
这些偏差就是所谓的时差
全球共分为24 个时区
为了照顾到各地区的使用方便,又使其他地方的人容易将本地的时间换算到别的地方时
间上去。有关国际会议决定将地球表面按经线从东到西,划成一个个区域,并且规定相邻区
域的时间相差1 小时。在同一区域内的东端和西端的人看到太阳升起的时间最多相差不过1
小时。当人们跨过一个区域,就将自己的时钟校正1 小时(向西减1 小时,向东加1 小时),
跨过几个区域就加或减几小时。这样使用起来就很方便。现今全球共分为24 个时区。
间上去。有关国际会议决定将地球表面按经线从东到西,划成一个个区域,并且规定相邻区
域的时间相差1 小时。在同一区域内的东端和西端的人看到太阳升起的时间最多相差不过1
小时。当人们跨过一个区域,就将自己的时钟校正1 小时(向西减1 小时,向东加1 小时),
跨过几个区域就加或减几小时。这样使用起来就很方便。现今全球共分为24 个时区。
时区并不严格按南北直线来划分,而是按自然条件来划分
实用上常常1 个国家,或1 个省份同时跨着2 个或更多时区,为了照顾到行政上的方便,
常将1 个国家或1 个省份划在一起。所以时区并不严格按南北直线来划分,而是按自然条件
来划分。
常将1 个国家或1 个省份划在一起。所以时区并不严格按南北直线来划分,而是按自然条件
来划分。
例如,中国幅员宽广,差不多跨5 个时区,但为了使用方便简单,实际上在只用
东八时区的标准时,即北京时间为准。
东八时区的标准时,即北京时间为准。
北京时间比洛杉矶时间早15 或者16 个小时。具体和时令有关。
北京时间比纽约时间早12 或者13 个小时。具体和时令有关。
冬令时和夏令时
夏令时、冬令时的出现,是为了充分利用夏天的日照,所以时钟要往前拨快一小时,冬
天再把表往回拨一小时。其中夏令时从3 月第二个周日持续到11 月第一个周日。
冬令时: 北京和洛杉矶时差:16 北京和纽约时差:13
夏令时: 北京和洛杉矶时差:15 北京和纽约时差:12
天再把表往回拨一小时。其中夏令时从3 月第二个周日持续到11 月第一个周日。
冬令时: 北京和洛杉矶时差:16 北京和纽约时差:13
夏令时: 北京和洛杉矶时差:15 北京和纽约时差:12
使用new Date()就可以获取中国的当前时间
由于不同的时区的时间是不一样的,甚至同一个国家的不同城市时间都可能不一样
所以,在Java 中想要获取时间时,要重点关注一下时区问题。
默认情况下,如果不指明,在创建日期时,会使用当前计算机所在的时区作为默认时区
这也是为什么我们通过只要使用new Date()就可以获取中国的当前时间的原因。
时间戳(timestamp)
时间戳(timestamp)
一份能够表示一份数据在一个特定时间点已经存在的、完整的、可验证的数据。
通常是一个字符序列,唯一地标识某一刻的时间。
指格林威治时间1970 年01 月01 日00 时00 分00 秒(北京时间1970 年01 月01 日08 时00 分00 秒)起至现在的总秒数。
几种常见时间的含义和关系
GMT
代表
格林威治时间
格林尼治平时
格林尼治标准时间
(旧译)
格林威治标准时间
格林尼治平均时间
英语:Greenwich Mean Time,GMT
定义
是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间(当地的平太阳时),因为本初子午线被定义在通过那里的经线。
自1924 年2 月5 日开始,格林尼治天文台负责每隔一小时向全世界发放调时信息。
格林尼治平时的正午是指当平太阳横穿格林尼治子午线时的时间。
(也就是在格林尼治上空最高点时)
被原子钟报时的协调世界时(UTC)所取代
由于地球每天的自转是有些不规则的,而且正在缓慢减速,
因此格林尼治平时基于天文观测本身的缺陷,
已经被原子钟报时的协调世界时(UTC)所取代。
一般使用GMT+8 表示中国的时间,是因为中国位于东八区,时间上比格林威治时间快8 个小时。
CST
北京时间,China Standard Time,又名中国标准时间,是中国的标准时间。
在时区划分上,属东八区,比协调世界时早8 小时,记为UTC+8,与中华民国国家标准时间(旧
称“中原标准时间”)、香港时间和澳门时间和相同。
称“中原标准时间”)、香港时间和澳门时间和相同。
当格林威治时间为凌晨0:00 時,中国标准时间刚好为上午8:00。
UTC
协调世界时,又称世界标准时间或世界协调时间,简称UTC,从法文和英文而来
法文“Temps Universel Cordonné”
英文“Coordinated Universal Time”
协调世界时,以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。
台湾,采用CNS 7648 的《资料元及交换格式–资讯交换–日期及时间的表示法》(与ISO 8601 类似)称之为世界统一时间。
中国大陆,采用ISO 8601-1988 的国标《数据元和交换格式信息交换日期和时间表示法》(GB/T 7408)中称之为国际协调时间。
CET
欧洲中部时间(英語:Central European Time,CET)
比世界标准时间(UTC)早一个小时的时区名称之一。
它被大部分欧洲国家和部分北非国家采用。
冬季时间为UTC+1,夏季欧洲夏令时为UTC+2。
关系
CET=UTC/GMT + 1 小时CST=UTC/GMT +8 小时CST=CET+9
SimpleDateFormat是什么?
Java 提供的一个格式化和解析日期的工具类。
它允许进行
格式化(日期-> 文本)
解析(文本-> 日期)
规范化
SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。
SimpleDateFormat 主要可以在String 和Date之间做转换, 还可以将时间转换成不同时区输出
SimpleDateFormat 类把时间显示成需要的格式
在日常开发中,经常会用到时间,我们有很多办法在Java 代码中获取时间。
但是不同的方法获取到的时间的格式都不尽相同,这时候就需要一种格式化工具,把时间显示成需要的格式。
最常用的方法就是使用SimpleDateFormat 类
日期和时间模式表达方法
在使用SimpleDateFormat 时,需要通过字母来描述时间元素,并组装成想要的日期和时间模式。
常用的时间元素和字母的对应表如下:
常用的时间元素和字母的对应表如下:
模式字母通常是重复的,其数量确定其精确表示。
常用的输出格式的表示方法
常用的输出格式的表示方法
yyyy 和YYYY 有什么区别?
使用SimpleDateFormat时,需要通过字母来描述时间元素,并组装成想要的日期和时间模式。
常用的时间元素和字母的对应表(JDK 1.8)如下:
常用的时间元素和字母的对应表(JDK 1.8)
可以看到,y 表示Year ,而Y 表示Week Year
什么是Week Year
不同的国家对于一周的开始和结束的定义是不同的。
在中国,把星期一作为一周的第一天
在美国,他们把星期日作为一周的第一天
同样,如何定义哪一周是一年当中的第一周?这也是一个问题,有很多种方式。
比如下图是2019 年12 月-2020 年1 月的一份日历。
比如下图是2019 年12 月-2020 年1 月的一份日历。
到底哪一周才算2020 年的第一周呢?不同的地区和国家,甚至不同的人,都有不同的理解。
1、1 月1 日是周三,到下周三(1 月8 日),这7 天算作这一年的第一周。
2、因为周日(周一)才是一周的第一天,所以,要从2020 年的第一个周日(周一)开始往后推7 天才算这一年的第一周。
3、因为12.29、12.30、12.31 是2019 年,而1.1、1.2、1.3 才是2020 年,而1.4周日是下一周的开始,所以,第一周应该只有1.1、1.2、1.3 这三天。
ISO 8601
ISO 8601是什么?
因为不同人对于日期和时间的表示方法有不同的理解,于是,大家就共同制定了了一个国际规范:ISO 8601 。
国际标准化组织的国际标准ISO 8601 是日期和时间的表示方法,。
全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》
在ISO 8601中,对于一年的第一个日历星期有以下四种等效说法
1,本年度第一个星期四所在的星期;
2,1 月4 日所在的星期;
3,本年度第一个至少有4 天在同一星期内的星期;
4,星期一在去年12 月29 日至今年1 月4 日以内的星期;
根据这个标准,可以推算出:
2020 年第一周:2019.12.29-2020.1.4。
所以,根据ISO 8601 标准,2019 年12 月29 日、2019 年12 月30 日、2019 年12 月31 日这两天,
其实不属于2019 年的最后一周,而是属于2020 年的第一周。
JDK 针对ISO 8601 提供的支持
根据ISO 8601 中关于日历星期和日表示法的定义,2019.12.29-2020.1.4 是2020年的第一周。
希望输入一个日期,然后程序告诉,根据ISO 8601 中关于日历日期的定义,这个日期到底属于哪一年。
比如我输入2019-12-20,他告诉我是2019;
而我输入2019-12-30 的时候,他告诉我是2020。
为了提供这样的数据,Java 7 引入了「YYYY」作为一个新的日期模式来作为标识。
使用「YYYY」作为标识,再通过SimpleDateFormat 就可以得到一个日期所属的周属于哪一年了。
所以,当要表示日期时,一定要使用yyyy-MM-dd ,而不是YYYY-MM-dd,这两者的返回结果大多数情况下都一样,但是极端情况就会有问题了。
SimpleDateFormat 用法
format 方法
将一个Date 类型转化成String 类型,并且可以指定输出格式。
//Date 转String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);
以上代码,转换的结果是:2018-11-25 13:00,日期和时间格式由"日期和时间模式"字符串指定。
如果你想要转换成其他格式,只要指定不同的时间模式就行了。
parse 方法
将一个String 类型转化成Date 类型。
// String 转Data
System.out.println(sdf.parse(dataStr));
System.out.println(sdf.parse(dataStr));
SimpleDateFormat获取并输出不同时区的时间
那么,如何在Java 代码中获取不同时区的时间呢?SimpleDateFormat 可以实现这个功能。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));
以上代码,转换的结果是: 2018-11-24 21 00 。
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));
以上代码,转换的结果是: 2018-11-24 21 00 。
既中国的时间是11 月25 日的13 点,而美国洛杉矶时间比中国北京时间慢了16 个小时(这还和冬夏令时有关系,就不详细展开了)。
尝试打印一下美国纽约时间(America/New_York)。纽约时间是2018-11-25 00 00。纽约时间比中国北京时间早了13 个小时。
SimpleDateFormat 的线程安全性问题
很多人愿意使用如下方式定义SimpleDateFormat
SimpleDateFormat 比较常用
在一般情况下,一个应用中的时间显示模式都是一样的
代码说明
很多人愿意使用如下方式定义SimpleDateFormat
这种定义方式,存在很大的安全隐患。
问题重现:SimpleDateFormat使用不当出现线程安全问题
来看一段代码,以下代码使用线程池来执行时间输出。
代码段1
代码段2:以上代码,其实比较简单,很容易理解。
就是循环一百次,每次循环时,都在当前时间基础上增加一个天数(这个天数随着循环次数而变化),
然后把所有日期放入一个线程安全的、带有去重功能的Set 中,然后输出Set 中元素个数。
正常情况下,以上代码输出结果应该是100。但是实际执行结果是一个小于100 的数字。
原因就是,因为SimpleDateFormat 作为一个非线程安全的类,被当做了共享变量在多个线程中进行使用,这就出现了线程安全问题。
这是一个看上去功能比较简单的类,但是,一旦使用不当也有可能导致很大的问题。
在并发场景中SimpleDateFormat 是不能保证线程安全的,需要开发者自己来保证其安全性。
在Java 开发手册的第一章第六节——并发处理中关于这一点也有明确说明:
在Java 开发手册中,有如下明确规定
那么,接下来我们就来看下到底是为什么,以及该如何解决。
SimpleDateFormat线程不安全的原因,SimpleDateFormat 底层原理?
通过以上代码,我们发现了在并发场景中使用SimpleDateFormat 会有线程安全问题。
其实,JDK 文档中已经明确表明了SimpleDateFormat 不应该用在多线程场景中:
Date formats are not synchronized. It is recommended to create
separate format instances for each thread. If multiple threads access a for
mat concurrently, it must be synchronized externally.
separate format instances for each thread. If multiple threads access a for
mat concurrently, it must be synchronized externally.
那么接下来分析下为什么会出现这种问题 ?
SimpleDateFormat 底层到底是怎么实现的?
SimpleDateFormat 类中format 方法的实现其实就能发现端倪。
SimpleDateFormat 类中format 方法
SimpleDateFormat 中的format 方法在执行过程中,会使用一个成员变量calendar来保存时间。
这其实就是问题的关键。
由于我们在声明SimpleDateFormat 的时候,使用的是static 定义的。
那么这个SimpleDateFormat 就是一个共享变量,随之,SimpleDateFormat 中的calendar 也就可以被多个线程访问到。
假设线程1 刚刚执行完calendar.setTime 把时间设置成2018-11-11,还没等执行完,
线程2 又执行了calendar.setTime 把时间改成了2018-12-12。这时候线程1 继续往下
执行,拿到的calendar.getTime 得到的时间就是线程2 改过之后的。
线程2 又执行了calendar.setTime 把时间改成了2018-12-12。这时候线程1 继续往下
执行,拿到的calendar.getTime 得到的时间就是线程2 改过之后的。
除了format 方法以外,SimpleDateFormat 的parse 方法也有同样的问题。
所以,不要把SimpleDateFormat 作为一个共享变量使用。
如何解决SimpleDateFormat线程不安全的问题 ?
解决方案1:使用局部变量
使用局部变量
SimpleDateFormat 变成了局部变量,就不会被多个线程同时访问到了,就避免了线程安全问题。
解决方案2:使用synchronized 加同步锁
是对于共享变量进行加锁。
通过加锁,使多个线程排队顺序执行。避免了并发导致的线程安全问题。
其实以上代码还有可以改进的地方,就是可以把锁的粒度再设置的小一点,可以只对simpleDateFormat.format 这一行加锁,这样效率更高一些。
解决方案3:使用Threadlocal 为每一个线程单独创建一个
ThreadLocal 可以确保每个线程都可以得到单独的一个SimpleDateFormat 的对象,那么自然也就不存在竞争问题了。
使用ThreadLocal
用ThreadLocal 来实现其实是有点类似于缓存的思路,
每个线程都有一个独享的对象,避免了频繁创建对象,也避免了多线程的竞争。
以上代码也有改进空间,SimpleDateFormat 的创建过程可以改为延迟加载
解决方案4:Java8使用DateTimeFormatter
如果是Java8 应用,可以使用DateTimeFormatter 代替SimpleDateFormat
这是一个线程安全的格式化工具类。
就像官方文档中说的,这个类simple beautiful strong immutable thread-safe。
使用案例
如果是Java8 应用,可以使用DateTimeFormatter 代替SimpleDateFormat
Java 8 中的时间处理
Java8以前的版本,日期时间API 存在诸多问题
非线程安全
java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java 日期类最大的问题之一。
设计很差
Java 的日期/时间类的定义并不一致,在java.util 和java.sql 的包中都有日期类,此外用于格式化和解析的类在java.text 包中定义。
java.util.Date 同时包含日期和时间,而java.sql.Date 仅包含日期,将其纳入java.sql 包并不合理。
另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
时区处理麻烦
日期类并不提供国际化,没有时区支持,
因此Java 引入了java.util.Calendar 和java.util.TimeZone 类,但他们同样存在上述所有的问题。
Java8,使用新的时间及⽇期API
Java 8 通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
Java8 提供了一套新的时间处理API,这套API 比以前的时间处理API 要友好的多。
新的时间及⽇期API 位于java.time 包中,
新的java.time 包,涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。
该包中有哪些重要的类。分别代表了什么?
LocalDate
表⽰⽇期,只包含⽇期,年⽉⽇
⽐如: 2016-10-20
LocalTime
表⽰时间,只包含时间,时分秒
⽐如: 23:10
LocalDateTime
包含⽇期 + 时间
⽐如: 2016-10-20 23:21
Instant: 时间戳
Duration: 持续时间, 时间差
Period: 时间段
ZoneOffset: 时区偏移量, ⽐如: +8:00
ZonedDateTime: 带时区的时间
Clock: 时钟, ⽐如获取⽬前美国纽约的时间
新的时间及⽇期API的使用案例
获取当前时间
在Java8 中,使用如下方式获取当前时间:
LocalDate today = LocalDate.now();
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
System.out.printf("Year : %d Month : %d day : %d t %n", year,month, day);
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
System.out.printf("Year : %d Month : %d day : %d t %n", year,month, day);
创建指定日期的时间
LocalDate date = LocalDate.of(2018, 01, 01);
检查闰年
直接使⽤LocalDate 的isLeapYear 即可判断是否闰年:
LocalDate nowDate = LocalDate.now();
//判断闰年
boolean leapYear = nowDate.isLeapYear();
//判断闰年
boolean leapYear = nowDate.isLeapYear();
计算两个⽇期之间的天数和⽉数
在Java 8 中可以⽤java.time.Period 类来做计算。
Period period = Period.between(LocalDate.of(2018, 1, 5),LocalDate.of(2018,2, 5));
如何在东八区的计算机上获取美国时间 ?
在Java8 中
加入了对时区的支持
带时区的时间为分别
ZonedDate
ZonedTime
ZonedDateTime
其中,每个时区都对应着ID
地区ID都为“ { 区域} / { 城市} ” 的格式
如Asia/Shanghai、America/Los_Angeles 等。
直接使用以下代码即可输出美国洛杉矶的时间:
LocalDateTime now = LocalDateTime.now(ZoneId.of("America/Los_Angeles"));
System.out.println(now);
System.out.println(now);
为什么以下代码无法获得美国时间呢?
System.out.println(Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles")).getTime());
当使用System.out.println 来输出一个时间时,它会调用Date 类的toString 方法,而该方法会读取操作系统的默认时区来进行时间的转换。
Date 类的toString 方法
主要代码如上。
也就是说,如果想要通过System.out.println 输出一个Date 类时,
输出美国洛杉矶时间的话, 就需要想办法把defaultTimeZone改为America/Los_Angeles。
通过阅读Calendar 的源码,发现,getInstance 方法虽然有一个参数可以传入时区,但是并没有将默认时区设置成传入的时区。
而在Calendar.getInstance.getTime 后得到的时间只是一个时间戳,其中未保留任何和时区有关的信息,
所以,在输出时,还是显示的是当前系统默认时区的时间。
语法糖
是什么?
语法糖(Syntactic Sugar),也称糖衣语法
是由英国计算机学家Peter.J.Landin发明的一个术语
指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
在编程领域,除了语法糖,还有语法盐和语法糖精的说法
所熟知的编程语言中几乎都有语法糖
语法糖的多少是评判一个语言够不够牛逼的标准之一。
所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。
好处
语法糖让程序更加简洁,有更高的可读性。
语法糖的存在主要是方便开发人员使用
更方便程序员使用
语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。
有了这些语法糖,在日常开发时,可大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。
Java 是一个“低糖语言”
其实从Java 7 开始,Java 语言层面上一直在添加各种糖
从Java 7 开始,Java 语言中的语法糖在逐渐丰富
比如,java7的switch开始支持String
主要是在“Project Coin”项目下研发。
尽管现在Java 有人还是认为现在的Java 是低糖,未来还会持续向着“高糖”的方向发展。
解语法糖
语法糖的存在主要是方便开发人员使用。
但是,其实Java 虚拟机并不支持这些语法糖。
要想被执行,需要进行解糖,即转成JVM 认识的语法。
这些语法糖在编译阶段就会被还原成简单的基础语法结构,
把语法糖解糖之后,这些语法其实都是一些其他更简单的语法构成的。
编译与解语法糖
Java 语言中,javac 命令可以将后缀名为.java 的源文件编译为后缀名为.class的可以运行于JVM的字节码。
如果去看com.sun.tools.javac.main.JavaCompiler 的源码,
在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。
Java 中最常用的语法糖
糖块一、switch 支持String 与枚举
背景
Java 中的switch 自身原本就支持基本类型
比如int、char 等。
对于int 类型,直接进行数值的比较。
对于char 类型则是比较其ascii 码。
对于编译器来说,switch 中其实只能使用整型,任何类型的比较都要转换成整型。
比如byte,short,char(ackii 码是整型)以及int。
Java 7 中switch 开始支持String
switch 对String的支持
样例代码
子主题
反编译后内容如下:
反编译后内容如下:
看到这个代码,原来字符串的switch 是通过equals()和hashCode()方法来实现的。
还好hashCode()方法返回的是int,而不是long。
仔细看下可以发现,进行switch 的实际是哈希值,然后通过使用equals 方法比较进
行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。
行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。
因此它的性能是不如使用枚举进行switch 或者使用纯整数常量,但这也不是很差。
糖块二、泛型
很多语言都是支持泛型的,不同的编译器对于泛型的处理方式是不同的
两种编译器处理泛型的方式
Code specialization
C++和C#是使用Code specialization 的处理机制
Code sharing
而Java 使用的是Code sharing 的机制
为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。
将多种泛型类形实例,映射到唯一的字节码表示,是通过类型擦除实现的。
通过类型擦除的方式,进行解语法糖
类型擦除(type erasue)
JVM根本不认识Map<String, String> map 这样的语法,需要在编译阶段,通过类型擦除的方式,进行解语法糖。
类型擦除的主要过程如下
1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
2.移除所有的类型参数。
JVM虚拟机中,没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时,都会被擦除,泛型类,并没有自己独有的Class 类对象。
比如,并不存在List<String>.class 或是List<Integer>.class,而只有List.class。
代码说明
1、Map<String ,String>map的例子
样例代码
Map<String, String> map = new HashMap<String, String>();
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");
解语法糖之后会变成:
Map map = new HashMap();
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");
2、public static <A extends Comparable<A>> A max
样例代码
样例代码
类型擦除后会变成:
类型擦除后会变成:
可能遇到的坑
当泛型遇到重载
public class GenericTypes{
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}}
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}}
上面这段代码,有两个重载函数,因为参数类型不同
一个是List<String>
另一个是List<Integer>
但是,这段代码是编译通不过的。
参数List 和List 编译之后都被擦除了,
变成了一样的原生类型List,
擦除动作导致这两个方法的特征签名变得一模一样。
当泛型,遇到catch
当泛型遇到catch ,泛型的类型参数不能用在Java 异常处理的catch 语句中。
因为,异常处理是由JVM 在运行时刻来进行的。
由于类型信息被擦除,JVM 是无法区分两个异常类型MyException<String>和MyException<Integer>的。
当泛型内包含静态变量
以上代码输出结果为:2!
由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。
糖块三、自动装箱与拆箱
糖块四、方法变长参数
可变参数是什么?
可变参数(variable arguments)
在Java 1.5 中引入的一个特性。
它允许一个方法把任意数量的值作为参数。
可变参数代码demo
看下以下可变参数代码,其中print 方法接收可变参数:
反编译后代码
反编译后代码:
从反编译后代码可以看出,可变参数在被使用时,
(1)创建一个数组,数组的长度就是调用该方法是传递的实参的个数,
(2)再把参数值全部放到这个数组当中,
(3)把这个数组作为参数传递到被调用的方法中。
PS:反编译后的print 方法声明中有一个transient 标识
糖块五 、 枚举
Java的枚举类型
Java SE5提供了一种新的类型
关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用
一种非常有用的功能
源码分析
要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?
答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢
简单的写一个枚举
反编译后代码内容如下:
反编译后代码内容如下:
通过反编译后代码我们可以看到,
public final class T extends Enum,
该类是继承了Enum类的,
final关键字,这个类也是不能被继承的。
当使用enmu来定义一个枚举类型时,编译器会自动创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
糖块六、内部类
内部类是什么?
又称为嵌套类
可以把内部类理解为外部类的一个普通成员
内部类也是一种语法糖
内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念
outer.java 里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class 文件了
outer.class
outer$inner.class。
所以,内部类的名字完全可以和它的外部类名字相同
内部类的名字完全可以和它的外部类名字相同
以上代码编译后会生成两个class 文件:
OutterClass$InnerClass.class
OutterClass.class
当尝试对OutterClass.class 文件进行反编译,命令行会打印以下内容:
Parsing OutterClass.class...
Parsing inner class OutterClass$InnerClass.class...
Generating OutterClass.jad 。
他会把两个文件全部进行反编译,然后一起生成一个OutterClass.jad 文件。
文件内容如下:
文件内容如下:
糖块七、条件编译
什么是条件编译
—般情况下,程序中的每一行代码都要参加编译。
但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译
此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃
条件编译具体在不同语言中是如何实现的呢?
在C 或C++中,
可以通过预处理语句来实现条件编译
在Java 中,
也可实现条件编译。
Java 语法的条件编译,是通过判断条件为常量的if 语句实现的。
根据if 判断条件的真假,编译器直接把分支为false 的代码块消除。
Java的条件编译具有局限性
通过该方式实现的条件编译,必须在方法体内实现,
而无法在正整个Java 类的结构或者类的属性上进行条件编译
这与C/C++的条件编译相比,确实更有局限性。
在Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。
其原理也是Java 语言的语法糖
举证说明
先来看一段代码
先来看一段代码
反编译后代码如下:
反编译后代码如下:
首先,在反编译后的代码中没有System.out.println("Hello, ONLINE!");,这其实就是条件编译。
当if(ONLINE)为false 时,编译器就没有对其内的代码进行编译。
糖块八、断言
assert 关键字是什么?
从JAVA SE 1.4 引入的
为了避免和老版本的Java代码中使用了assert 关键字导致错误,Java 在执行时,默认是不启动断言检查的
这个时候, 所有的断言语句都将忽略!
如果要开启断言检查, 则需要用开关-enableassertions 或-ea 来开启。
关于断言的代码演示
看一段包含断言的代码:
看一段包含断言的代码:
反编译后代码如下:
很明显,反编译之后的代码要比我们自己的代码复杂的多。
很明显,反编译之后的代码要比我们自己的代码复杂的多。
使用了assert 这个语法糖节省了很多代码。
断言的底层
其实断言的底层实现就是if 语言
如果断言结果为true,则什么都不做,程序继续执行,
如果断言结果为false,则程序抛出AssertError 来打断程序的执行。
-enableassertions 会设置$assertionsDisabled 字段的值。
糖块九、数值字面量
数值字面量是什么?
在java 7 中支持数值字面量,
不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。
这些下划线不会对字面量的数值产生影响,目的就是方便阅读。
代码讲解
数值字面量代码
数值字面量
反编译后:
反编译后就是把_删除了。
也就是说,编译器并不认识在数字字面量中的_,需要在编译阶段把他去掉。
糖块十、增强for 循环(for-each)
增强for 循环(for-each)
相信大家都不陌生,日常开发经常会用到的
他会比for循环要少写很多代码
语法糖背后是如何实现的呢?
for-each代码
for-each代码
反编译后代码
反编译后代码
代码很简单,for-each 的实现原理其实就是使用了普通的for 循环和迭代器。
会抛出ConcurrentModificationException 异常?
例子
会抛出ConcurrentModificationException 异常。
Iterator在工作时,是不允许被迭代的对象被改变的
工作在一个独立的线程中,并且拥有一个mutex 锁。
Iterator 被创建之后,会建立一个指向原来对象的单链索引表
当原来的对象数量发生变化时,这个索引表的内容不会同步改变
所以当索引指针往后移动时,就找不到要迭代的对象
按照fail-fast 原则,Iterator 会马上抛出java.util.ConcurrentModificationException 异常。
所以Iterator在工作时,是不允许被迭代的对象被改变的。
可以使用Iterator 本身的方法remove()
但可以使用Iterator 本身的方法remove(),来删除对象,
Iterator.remove() 方法
删除当前迭代对象
维护索引的一致性
糖块十一、try-with-resource
糖块十二、Lambda 表达式
Labmda 表达式不是匿名内部类的语法糖,但是他也是一个语法糖。
实现方式其实是依赖了几个JVM 底层提供的lambda 相关api。
代码说明
简单的lambda 表达式
遍历一个list:
遍历一个list:
反编译后代码如下:
反编译后代码如下:
可以看到,在forEach 方法中,其实是调用了java.lang.invoke.LambdaMetafactory#metafactory 方法,
该方法的第四个参数implMethod 指定了方法实现。
可以看到这里,其实是调用了一个lambda$main$0 方法进行了输出。
稍微复杂一点的lambda 表达式
先对List 进行过滤,然后再输出:
先对List 进行过滤,然后再输出:
反编译后代码如下:
两个lambda 表达式分别调用了lambda$main$1 和lambda$main$0 两个方法。
所以,lambda 表达式的实现其实是依赖了一些底层的api,
在编译阶段,编译器会把lambda 表达式进行解糖,转换成调用内部api 的方式。
为啥说他并不是内部类的语法糖呢
内部类在编译之后,会有两个class 文件
但是,包含lambda 表达式的类编译后只有一个文件。
JAVA复制
将一个对象的引用复制给另外一个对象,一共有三种方式。
第一种方式是直接赋值
在Java中,A a1 = a2,这实际上复制的是引用,也就是说a1和a2指向的是同一个对象。
因此,当a1变化的时候,a2里面的成员变量也会跟着变化。
第二种方式是浅拷贝
浅复制(复制引用但不复制引用的对象)
创建一个新对象,然后将当前对象的非静态字段复制到该新对象,
如果字段是值类型的,那么对该字段执行复制;
如果该字段是引用类型的话,则复制引用但不复制引用的对象。
因此,原始对象及其副本引用同一个对象。
第三种是深拷贝。
深复制(复制对象和其应用对象)
深拷贝不仅复制对象本身,而且复制对象包含的引用指向的所有对象。
序列化(深clone一中实现)
在Java语言里深复制一个对象,
常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。
常见问题
面向对象常见问题
接口和类的区别?如何定义不同的接口和类?
类的实现与继承有什么关系?
抽象类必须要有抽象方法吗?
抽象类能使用 final 修饰吗?
final 在 java 中有什么作用?
接口和抽象类有什么区别?
普通类和抽象类有哪些区别?
Java是否支持多继承?
类不支持多继承,接口支持多继承。
基础的常见问题
JDK 和 JRE 有什么区别?
常见类的常见问题
== 和 equals 的区别是什么?
String 属于基础的数据类型吗?
java 中操作字符串都有哪些类?它们之间有什么区别?
两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
如何将字符串反转?
String 类的常用方法都有那些?
String str="i"与 String str=new String(“i”)一样吗?
java 中的 Math.round(-1.5) 等于多少?
其他问题
对象拷贝
为什么要使用克隆?
如何实现对象克隆?
深拷贝和浅拷贝区别是什么?
收藏
0 条评论
下一页