目录
Java语言发展状况
版本 | 年份 | 语言新特性 | 类与接口数量 |
---|---|---|---|
1.0 | 1996 | 语言本身 | 211 |
1.1 | 1997 | 内部类 | 477 |
1.2 | 1998 | strictfp 修饰符 | 1524 |
1.3 | 2000 | 无 | 1840 |
1.4 | 2002 | 正则表达式,异常链,NIO,日志类,XML解析器,XLST转换器 | 2723 |
5.0 | 2004 | 动态注解、泛型、“ for each” 循环、 可变元参数、 自动装箱、元数据、 枚举、 静态导入 | 3279 |
6 | 2006 | 提供动态语言支持、提供编译API和卫星HTTP服务器API,改进JVM的锁,同步垃圾回收,类加载 | 3793 |
7 | 2011 | 基于字符串的 switch、 钻石操作符、 二进制字面量、异常处理改进、提供GI收集器、加强对非Java语言的调用支持(JSR-292,升级类加载架构) | 4024 |
8 | 2014 | Lambda 表达式、方法引用、默认方法、新工具、Stream API、Date Time API 、Optional 类、Nashorn, JavaScript 引擎 | 4240 |
… |
Java简介
Java 是由 Sun Microsystems 公司于 1995 年 5 月推出的高级程序设计语言。
2009年,Sun 公司被 Oracle (甲骨文)公司收购,Java 也随之成为 Oracle 公司的产品。
Java语言的特点
- 简单性
- 面向对象
- 分布式
- 健壮性
- 安全性
- 体系结构中立
- 可移植性
- 解释型
- 高性能
- 多线程
- 动态性
JRE和JDK
JRE(Java Runtime Environment),Java运行环境;
包含JVM标准实现及Java核心类库,不包含任何开发工具(如编译器和调试器)。
JDK(Java SE Development Kit),Java开发工具包;
JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具。
JVM(Java Virtual Machine),Java虚拟机;
Java运行时的环境,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。调用Java命令运行java程序时,该命令会启动一个Java虚拟机进程,不管该程序多么负责,启动了多少个线程,它们都处于该Java虚拟机进程里。
JDK目录介绍:
bin: 最主要的是编译器(javac.exe)
include: java和JVM交互用的头文件
lib:类库
jre: java运行环境
src.zip 文件中包含了所有公共类库的源代码。
Java数据类型和运算符
Java标识符
标识符就是用于给程序中变量,方法,类命名的符号。Java语言的标识符必须是以 字母、下画线(_)、美元符 开头,后面可以跟任意数目的字母、数字、下画线(_)和美元符。此处的字母并不限于26个英文字母,而且可以包含中文字符、日文字符等。
Java语言是区分大小写的。 因此abc和ABC是两个不同的标识符。
使用时,注意以下规则:
- 标识符可以由字母、数字、下画线(_)和美元符组成,其中数字不能开头。
- 标识符不能是Java关键字和保留字,但可以包含关键字和保留字。
- 标识符不能包含空格。
- 标识符只能包含美元符号,不能包含@,#等其他特殊符号。
Java关键字
Java语言中有一些具有特殊用途的单词被称为关键字(keyword)。当定义标识符时,不要让标识符和关键字相同,否则会引起错误。
Java的所有关键字都是小写。TRUE,FALSE和NULL 都不是java关键字。
Java中一共包含50个关键字。
分类 | 关键字 | 个数 |
---|---|---|
访问控制 | private, default, protected, public | 4个 |
类,方法,变量控制符 | new, class, abstract, implements, extends, interface, final, static, native, strictfp, volatile, synchronized, transient | 13个 |
程序控制 | break, continue, return, do, while, if, else, for, instanceof, switch, case , default | 12个 |
错误处理 | try, catch, throw, throws, finally | 5个 |
包相关 | import, package | 2个 |
基本类型 | byte, short, int, long, char, float, double, boolean | 8个 |
变量引用 | super, this, void | 3个 |
保留字 | goto, const (Java还未使用这两个关键字) | 2个 |
Java 5新增 | enum | 1个 |
Java数据类型分类
Java语言是强类型(strongly typed)语言,强类型包含两个方面的含义:
- 所有变量必须先声明,后使用
- 指定类型的变量只能接受类型与之匹配的值。(这意味着,每个变量和每个表达式都有一个在编译时就确定的类型)
Java语言支持的类型分为两类: 基本类型(Primitive Type)和 引用类型(Reference Type)
8种基本类型 ( primitive type )
在 Java 中,一共有 8种基本类型 ( primitive type ) :
- 4种整数类型(byte, short, int, long)
整型用于表示没有小数部分的数值, 它允许是负数。在 Java 中, 整型的范围与运行 Java 代码的机器无关。长整型数值有一个后缀L 或 l
。 - 2种浮点类型(float, double)
Java的浮点类型有固定的表数范围和字段长度,与机器无关。
浮点类型用于表示有小数部分的数值。float 类型的数值有一个后缀F 或 f
(例如, 3.14F)。没有后缀 F 的浮点数值(如 3.14 ) 默认为 double 类型。当然,也可以在浮点数值后面添加后缀D 或 d
。 - 1 种用于表示 Unicode 编码的字符单元的字符类型 char
表示单个的字符,字符型值必须使用单引号(’)括起来。
Java语言使用16位的Unicode的字符集作为编码格式,而Unicode被设计成支持世界上所有书面语言的字符,包括中文字符,因此Java程序支持各种语言的字符。 - 1 种用于表示真值的 boolean 类型。
在基本JAVA类型中,如果不明确指定,整数型的默认是int类型,带小数的默认是double类型。
例如,当定义32位的二进制整数时,最高位就是符号位,当符号位是1时,只表明它是一个负数。
尽管他们的默认值看起来不一样,但在内存中都是 0。
包装类在下面有介绍。
引用类型
包括: 类, 接口和数组类型,还有一种特殊的null类型。 所谓引用数据类型就是对一个对象的引用。对象包括实例和数组两种。实际上引用类型就是一个指针(java语言中不再使用指针这个说法)。
空类型(null type)就是null值的类型,这种类型没有名称。因为null类型没有名称,所以不可能声明一个null类型的变量或者转换到null类型。
空引用(null)是null类型变量唯一的值。空引用(null)可以转换为任何引用类型。不能转换成基本类型,因此不要把一个null值赋给基本数据类型的变量。
变量和常量
变量:
声明一个变量之后,必须用赋值语句对变量进行显式初始化, 千万不要使用未初始化的变量。例如, Java 编译器认为下面的语句序列是错误的:
int a;
System.out.println(a);
常量:
在 Java 中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为类常量。可以使用关键字 static final设置一个类常量,常量名使用全大写。
Java基本类型的类型转换
自动类型转换
当把一个表数范围较小的数值或变量直接赋值给另一个表数范围的大的变量时,系统会进行自动转换。
强制类型转化
与自动类型转换相反。
强制类型转换的语法格式是:(targetType)value,强制类型转换的运算符是圆括号()。
在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。
double b = 9.997;
int c = (int) b;
System.out.println(c);
// 强制类型转换通过截断小数部分将浮点值转换为整型
// 9
// 如果想对浮点数进行舍人运算, 以便得到最接近的整数(在很多情况下, 这种操作更有用,) 那就需要使用 Math.round 方法
int d = (int) Math.round(b);
System.out.println(d);
// 10
表达式类型的自动提升
当一个算术表达式包含多个基本类型的值时,整个算术表达式的数据类型将会自动提升:
- 所有的byte类型、short类型和char类型将被提升到int类型
- 整个算术表达式的数据类型自动提升到与表达式中最高等级操作数同样的类型。具体看上面自动类型转换的图。
运算符
Java 语言中的运算符可分为如下几种:
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- 位运算符
- 类型相关运算符
算术运算符
分别有:+、-、*、/、%、++、– 等。
注意,自加和自减只能用于操作变量。
自加
++: 自加。
要点:
- 自加是单目运算符,只能操作一个操作数
- 自加运算符,只能操作单个数值型(整型、浮点型都行)的变量,不能操作变量或表达式
详细解释:
count++是一个表达式,是有返回值的,它的返回值就是count自加前的值,Java对自加是这样处理的:首先把count的值(注意是值,不是引用)拷贝到一个临时变量区,然后对count变量加1,最后返回临时变量区的值。
运算符可以出现在操作数的左边或者右边,效果不同。
- 如果把++放在左边,则先把操作数加1,然后才把操作数放入表达式中运算
- 如果把++放在右边,则先把操作数放入表达式中运算,然后才把操作数加1
Java中for循环的i++和++i区别
++i是先执行 i = i +1 再使用 i 的值;
而 i++ 是先使用 i 的值再执行 i = i + 1;
语法格式:
for(a; b; c){
d;
}
for循环的执行顺序如下:
1. 进入循环执行a;只是进入的时候执行
2. 执行b; //条件为真才执行d,不然跳出for了
3. 执行d;
4. 执行c;
5. 再回到第2步开始执行
来看下面的实例:
for(int i = 0; i < 10;i++){
System.out.println(i);
}
相当于:
for(int i = 0; i < 10;){
System.out.println(i);
i++;
}
或者
for(int i = 0; i<10; ++i){
System.out.println(i);
}
相当于:
for(int i =0; i < 10;){
System.out.println(i);
++i;
}
打印出来的信息是:
打印信息证明了,在循环体中,i++和++i的作用是一样的。
但是肯定有一定区别的,所以把循环耗时也打印出来了。
/**
* 对比性能
*/
public static void perf() {
long start1 = System.nanoTime();
for (int i = 0; i < 10000; i++) {
if (i == 9999) {
System.out.println(i);
}
}
long end1 = System.nanoTime();
System.out.println("i++执行耗时:" + (end1 - start1));
long start2 = System.nanoTime();
for (int i = 0; i < 10000; ++i) {
if (i == 9999) {
System.out.println(i);
}
}
long end2 = System.nanoTime();
System.out.println("++i执行耗时:" + (end2 - start2));
}
输出:
9999
i++执行耗时:297393
9999
++i执行耗时:157018
可以看出,就是运行时间的差别。
在Java中i++语句是需要一个临时变量取存储返回自增前的值,而++i不需要。这样就导致使用i++时系统需要先申请一段内存空间,然后将值塞如进去,最后不用了才去释放。多了这么一系列操作时间。
所以要仔细理解这句话:++i是先执行 i = i +1 再使用 i 的值,而 i++ 是先使用 i 的值再执行 i = i + 1;
自减
也是单目运算符,用法和自加基本相似,只是将操作数的值减1。
赋值运算符
用=作为赋值运算符
比较运算符
分别有:>、>=、<、<=、== 等
逻辑运算符
分别有:&&、&、||、|、!、^ 等
^: 异或。当两个操作数不同时才返回true,如果两个操作数相同则返回false。
位运算符
分别有:&、|、~、<<、>>、>>>、^ 等
符号 | 作用 |
---|---|
& | 按位与。当两位同时为1时才返回1 |
| | 按位或。只要有一位为1时即可返回1 |
~ | 按位非。单目运算符,将操作数的每个位(包括符号位)全部取反 |
^ | 按位异或,当两位相同时返回0,不同时返回1 |
<< | 左移运算符 |
>> | 右移运算符 |
>>> | 无符号右移运算符 |
流程控制与数组
switch分支语句
switch语句由一个控制表达式和多个case标签组成,switch语句后面的控制表达式的数据类型只能是:
- byte、short、char、int四种基本类型
- 枚举类型
- java.lang.String类型 (从Java7才允许)
不能是boolean类型。
语法格式如下:
switch (expression)
{
case condition1:
{
statement(s);
break;
}
case condition2:
{
statement(s);
break;
}
...
case conditionN:
{
statement(s);
break;
}
default:
{
statement(s);
}
}
switch语句先对expression求值,然后依次匹配condition1、condition2、… 、conditionN等值,遇到匹配的值,即执行对应的执行体;如果所有的case标签后的值都与expression表达式的值不相等,则执行default标签后的代码块。
public static void main(String[] args) {
// byte, short, char, int
// java 7: java.lang.String
// enum
char expression = 'A';
switch (expression) {
case 'A':
System.out.println("A");
break;
case 'B':
System.out.println("B");
break;
case 'C':
System.out.println("C");
break;
default:
System.out.println("default");
}
String expression1 = "Hello";
switch (expression1) {
case "Hell":
System.out.println("Hell");
break;
case "Hello":
System.out.println("Hello");
break;
case "Hello World":
System.out.println("Hello World");
break;
default:
System.out.println("default");
}
}
输出:
A
Hello
注意,如果省略break,就可能会调入陷阱。 switch语句先对expression求值,然后拿到这个值和case标签后的值进行比较,一旦遇到相等的值,程序就开始执行这个case标签后的代码,不再判断与后面case、default标签的条件是否匹配,除非遇到break,才会结束。 如果去掉上面第二段switch语句的break,
String expression1 = "Hello";
switch (expression1) {
case "Hell":
System.out.println("Hell");
// break;
case "Hello":
System.out.println("Hello");
// break;
case "Hello World":
System.out.println("Hello World");
// break;
default:
System.out.println("default");
}
输出如下:
Hello
Hello World
default
面向对象
类和对象
类的修饰符: 可以是public , final , abstract, 或者完全省略这三个修饰符。
成员变量的修饰符: 可以省略,也可以是poblic, protected, private, static, final, 其中public, protected, private 最多只能出现其中之一,可以和static、final组合起来修饰成员变量。
格式
[修饰符] 类型 成员变量名 [=默认值]
方法的修饰符: 可以省略,也可以是poblic、protected、private、static、final、abstract, 其中public、protected、private 最多只能出现其中之一;abstract和final最多只能出现其中之一,它们可以和static 组合起来修饰方法。
格式:
[修饰符] 方法返回值类型 方法名(形参列表)
{
//由零到多条的可执行语句组成的方法体
}
注意: static修饰的方法和成员变量,既可以通过类来调用,也可以通过实例来调用;没有使用static修饰的普通方法和成员变量,只能通过实例来调用。
对象,引用和指针
对于代码:
Person p = new Person();
这行代码创建了一个Person实例,也称为 Person对象,这个Person对象被赋值给p变量(它是一个引用变量)
- 当把这个Person对象赋值给p变量时,系统是如何处理的呢?
Java让引用变量指向这个对象即可。也就是说,引用变量里面存储的只是一个引用,它指向一个实际的对象。
这个引用,它被存储在栈内存里,指向实际的Person对象;而真正的Person对象存储在堆内存中。
Java的引用变量和C语言的指针很像,他们都是存储一个地址,通过这个地址来引用到实际变量。实际上,java里的引用就是C语言里的指针,只是Java语言把这个指针封装起来,避免开发者进行繁琐的指针操作。
堆内存里的对象可以有多个引用,即多个引用变量指向用一个对象。
例如:
Person p2 = p;
如果堆内存里的对象,没有任何变量指向它,那么程序将无法再访问该对象,这个对象也就变成了垃圾,Java的垃圾回收机制将回收该对象,释放该对象所占的内存区。
因此,如果希望通知垃圾回收机制回收某个对象,只需要切断该对象的所有引用变量和它之间的关系即可,也就是把这些引用变量赋值为null。
对象的this引用
Java提供了一个this关键字,this关键字总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形:
- 构造器中引用该构造器正在初始化的对象
- 在方法中引用调用该方法的对象
this关键字最大的作用是:让类中一个方法,访问类里的另一个方法或实例变量。
this可以代表任何对象,当this出现在某个方法体中,它所代表的对象是不确定的,但它的类型是确定的:它所代表的只能是当前类的实例;只有当这个方法被调用时,它所代表的对象才被确定下来,谁在调用这个方法,this就代表谁。
Java允许对象的一个成员直接调用另一个成员,可以省略this前缀。(省略this前缀只是一种假象,虽然程序员省略了调用方法之前的this,但实际上这个this依然是存在的)
大部分时候,一个方法访问该类中定义的其他方法、成员变量时,加不加this前缀的效果是完全一样的。
延伸:
对于static修饰的方法而言,则可以使用类来调用该方法,如果在static修饰的方法中使用this关键字,则这个关键字就无法指向合适的对象。所以,static修饰的方法中不能使用this引用。
由于static修饰的方法中不能使用this引用,所以static修饰的方法不能访问不使用static修饰的普通成员,因此java语法规定: 静态成员不能直接访问非静态成员
方法的参数传递机制
Java里方法的参数传递方式只有一种:值传递。
就是将实际参数的副本传入方法内,而参数本身不会受到任何影响。
注意Java程序运行时,方法栈区的概念。
什么是值传递和引用传递?
值传递:是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。
引用传递:一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。所以对引用对象进行操作会同时改变原对象
全局变量和局部变量
成员变量:类变量 和 实例变量 (是否有static修饰)
局部变量:形参、方法局部变量、代码块局部变量 (除了形参之外,都必须要显示初始化)
- 形参:整个方法体内有效
- 代码块局部变量:只要离开了对应的代码块,这个局部变量就会被销毁。
- 方法局部变量: 其作用域从定义该变量开始,直到该方法结束。
在同一个类中,成员变量的作用域是整个类内有效,一个类中不能定义两个同名的成员变量,即使是一个类变量,一个实例变量也不行。
同一个方法里,不能定义两个同名的方法局部变量,方法局部变量和形参也不能同名;
同一个方法里,不同代码块中的局部变量可以同名;
如果先定义代码块局部变量,后定义方法局部变量,它们可以同名。
java允许成员变量和局部变量同名。
如果方法中的局部变量和成员变量同名,局部变量会覆盖成员变量,如果需要在方法中引用被覆盖的成员变量,则可以使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。
Java访问控制符
范围 | private | default | protected | public |
---|---|---|---|---|
同一个类中 | √ | √ | √ | √ |
同一个包中 | √ | √ | √ | |
子类中 | √ | √ | ||
全局范围内 | √ |
default: 包访问权限
如果一个java源文件里定义的所有类都没有使用public修饰,则这个源文件的文件名,可以是一切合法的文件名;如果一个java文件中定义了一个public修饰的类,则这个源文件的文件名必须要与public修饰的类的类名相同。
在这里需要注意包的概念:
Java引入了import关键字,可以用来向某个java文件中导入指定包层次下某个类或者全部类,import语句应该出现在package语句之后,类定义之前。
JDK1.5以后增加了静态导入的语法。用于导入指定类的某个静态成员变量、方法:
import static package.subpackage…ClassName.fieldName|methodName;
或导入全部的静态成员变量、方法:
import static package.subpackage…ClassName.*;
构造器
作用:创建对象时执行初始化。
一旦提供了自定义的构造器,系统就不再提供默认的构造器。
构造器重载:同一个类中有多个构造器,多个构造器的形参列表不同。
可以使用this关键字来调用另一个重载的构造器,只能在构造器中使用,而且必须作构造器执行体的第一条语句。
总结:
1、构造方法的出现是为了方便为对象的属性初始化值
2、一般在类中构造方法的顺序都是按照参数的个数去升序排序的
3、默认jvm会为每个类提供一个无参数构造方法,如果定义了有参构造方法那么JVM就不会在提供无参数构造了,所以如果我们定义了有参构造那么无参构造也要显示的定义出来。
面向对象特征
面向对象四大特性:封装,继承,多态,抽象
1. 封装
英文:Encapsulation
将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
良好的封装能够减少耦合。为了实现良好的封装,需要从以下两个方面考虑:
- 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问
- 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和控制。
2. 继承
英文:Inheritance
继承是从已有的类中派生出新的类,新的类继承父类的属性和行为,并能扩展新的能力,大大增加程序的重用性和易维护性。
Java语言使用extends
关键字实现继承,具有单继承的特点,每个子类只有一个直接父类。
如果Java类没有显示定义指定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此java.lang.Object是所有类的父类。要么是直接父类,要么是其间接父类。
super限定
super是java提供的一个关键字。用来限定该对象调用它从父类继承得到的实例变量或方法。正如this不能出现在static修饰的方法中,super也不能出现在static修饰的方法中。
static修饰的方法属于类的,该方法的调用者是一个类,而不是一个对象,因而super也失去了意义。
如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
如果子类里没有包含和父类同名的成员变量,那么在子类实例方法中访问该成员变量时,则无须显示使用super或父类名作为调用者。
如果在某个方法中访问名为a的成员变量,但没有显示指定调用者,则系统查找a的顺序为:
- 查找该方法中是否有名为a的局部变量
- 查找当前类中是否包含名为a的成员变量
- 查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能查找名为a的成员变量,则系统会出现编译错误。
如果被覆盖的是类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量。
在一个构造器中调用另一个重载的构造器,使用this调用来完成;在子类构造器中调用父类构造器,使用super调用来完成。
使用super调用父类构造器,也必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现。
创建任何对象,总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。
到底何时需要从父类派生出子类呢?
子类需要额外增加属性,而不仅仅是属性值得改变。
子类需要增加自己独有的行为方式(包括增加新的方法或者重写父类的方法)
Java重载和重写
方法重载(Overload): 是指同一个类中的多个方法具有相同的名字,但这些方法具有不同的参数列表,即参数的数量或参数类型不能完全相同,返回值类型可以相同也可以不相同
方法重写(Override):
如果子类中包含与父类同名的方法的现象称为 方法重写(Override),或 方法覆盖。需要遵循两同两小一大的规则:
两同:方法名相同,形参列表相同
两小:子类方法返回值类型应该比父类方法返回值类型更小或者相等;子类方法申明抛出的异常类应该比父类方法申明抛出的异常类要更小或者相等。
一大:子类方法的访问权限应该比父类方法的访问权限更大或者相等。
父类中private的方法不能被重写,如果子类中有相同的方法,只能算是子类新的方法。
当子类覆盖了父类方法后,子类对象将无法直接访问父类中被覆盖的方法,但可以在子类中调用父类中被覆盖的方法。通过super关键字。
3. 多态
英文:Polymorphism
多态是同一个行为具有多个不同表现形式的能力。在不修改程序代码的情况下改变程序运行时绑定的代码。
实现多态的三要素:继承、重写、父类引用指向子类对象。
- 静态多态性:通过重载实现,相同的方法有不同的參数列表,可以根据参数的不同,做出不同的处理。
- 动态多态性:在子类中重写父类的方法。运行期间判断所引用对象的实际类型,根据其实际类型调用相应的方法。
Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态。
重点:编译看左边,运行看右边。 父类型引用指向子类型对象,无法调用只在子类型里定义的方法。
public class Sub extends Base {
public String info = "sub";
public void sub() {
System.out.println("子类的普通方法");
}
public void test() {
System.out.println("子类覆盖父类的方法");
}
public static void main(String[] args) {
// 下面编译时和运行时类型完全一致,因此不会出现多态
Base base = new Base();
System.out.println(base.info);
base.base();
base.test();
Sub sub = new Sub();
System.out.println(sub.info);
sub.sub();
sub.test();
// 下面编译时和运行时类型不一致,因此出现多态
Base polymorphism = new Sub();
// 输出base -- 表明访问的是父类对象的实例变量
System.out.println(polymorphism.info);
// 下面执行从父类继承到的base()方法
polymorphism.base();
// 执行当前类的test()方法
polymorphism.test();
// 因为polymorphism的编译时类型是Base,没有提供sub()方法,会出现编译错误
// polymorphism.sub();
}
}
class Base {
public String info = "base";
public void base() {
System.out.println("父类的普通方法");
}
public void test() {
System.out.println("父类被覆盖的方法");
}
}
输出结果:
base
父类的普通方法
父类被覆盖的方法
sub
子类的普通方法
子类覆盖父类的方法
base
父类的普通方法
子类覆盖父类的方法
相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,就是多态(Polymorphism)。
当把子类对象赋值给父类引用变量时,被称为 向上转型,由系统自动完成。这种转型通常是成功的,这也从侧面证实了子类是一种特殊的父类。 这种转型只是表明这个引用变量的编译时类型是父类,但实际执行他的方法时,依然表现出子类对象的行为方式。
引用变量在编译阶段,只能调用其编译时类型所具备的方法,但运行时则执行它运行时类型的方法。
通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的变量。
如以上情况限定,如果需要引用变量调用其运行时类型的方法,只能通过强制类型转换的方式,借助强制类型转换符,即小括号()。
用法:(type)variable 用于将variable变量转换成type类型的变量。
注意:
基本类型之间的转换只能在数值类型之间进行。这里的数值类型指的是:整数型、浮点型、字符型。 数值型和布尔型之间不能进行类型转化。
引用类型之间的转化,只能在具有继承关系的两个类型之间进行。
public class ConversionTest {
public static void main(String[] args) {
double d = 13.4;
long i = (long) d;
System.out.println(i);
int in = 5;
// Cannot cast from int to boolean
// boolean b = (boolean) in;
Object ob = "Hello";
// String是Object的子类,可以强转
// 而且ob变量的实际类型也是String,运行时也会通过。
String s = (String) ob;
System.out.println(s);
Object obj = new Integer(5);
// obj的实际类型是Integer,运行时,下面会引发
// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
String sObj = (String) obj;
}
}
为了程序的健壮性,修改第20行代码
// 判断是否可以成功转换,避免ClassCastException异常
if(obj instanceof String){
String sObj = (String) obj;
}
把一个父类对象赋值给子类引用变量时,就需要强制类型转换,而且还可能在运行时产生ClassCastException异常,使用instanceof 运算符可以让强制类型转换更安全。
instanceof 运算符
它的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成是一种特殊的类),它用于判断前面的对象是否是后面的类,或者子类,实现类的实例。如果是,则返回true,否则返回false。
注意:
instanceof 运算符前面操作数的编译类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会出现编译错误。
4. 抽象
把客观事物用代码抽象出来。
初始化块:
它也可以对java对象进行初始化操作。
它的修饰符只能是static,使用static修改的初始化块被称为静态初始化块。也称为类初始化块。
初始化只在创建Java对象时隐式执行,而且在执行构造器之前执行。执行顺序:前面定义的先执行,后面定义的后执行。
Java对象初始化顺序
public class Test {
public static void main(String[] args) {
new Leaf();
new Leaf();
}
}
class Root {
static {
System.out.println("Root的静态初始化块");
}
{
System.out.println("Root的普通初始化块");
}
public Root() {
System.out.println("Root的无参构造函数");
}
}
class Mid extends Root {
static {
System.out.println("Mid的静态初始化块");
}
{
System.out.println("Mid的普通初始化块");
}
public Mid() {
System.out.println("Mid的无参构造函数");
}
public Mid(String msg) {
// 通过this调用同一类中重载的构造器
this();
System.out.println("Mid的带参构造函数,参数是: " + msg);
}
}
class Leaf extends Mid {
static {
System.out.println("Leaf的静态初始化块");
}
{
System.out.println("Leaf的普通初始化块");
}
public Leaf() {
super("测试");
System.out.println("Leaf的无参构造函数");
}
}
输出结果如下:
Root的静态初始化块
Mid的静态初始化块
Leaf的静态初始化块
Root的普通初始化块
Root的无参构造函数
Mid的普通初始化块
Mid的无参构造函数
Mid的带参构造函数,参数是: 测试
Leaf的普通初始化块
Leaf的无参构造函数
Root的普通初始化块
Root的无参构造函数
Mid的普通初始化块
Mid的无参构造函数
Mid的带参构造函数,参数是: 测试
Leaf的普通初始化块
Leaf的无参构造函数
结果分析:
第一次创建一个Leaf对象时,因为系统中还不存在Leaf类,因此需要先加载再初始化Leaf类,初始化Leaf类时,会先执行其顶层父类的静态初始化块,再执行其直接父类的静态初始化块,最后才执行本身的静态初始化块。
一旦Leaf类初始化成功后,Leaf类将在虚拟机中一直存在,因此第二次创建Leaf实例时无须再次对Leaf类进行初始化。
普通初始化块和构造器的执行顺序,每次创建Leaf对象时,都需要先先执行最顶层父类的初始化块、最顶层父类的构造器,然后执行父类的初始化块、构造器…直到执行当前类的初始化块、当前类的构造器。
执行顺序总结:
- 类初始化阶段: 先执行最顶层父类的静态初始化块,然后依次向下,直到执行当前类的静态初始化块。
- 对象初始化阶段: 先执行最顶层父类的初始化块、最顶层父类的构造器,然后依次向下,直到执行当前类的初始化块、当前类的构造器。
包装类 (Wrapper Class)
为了解决8种基本数据类型的变量不能当成Object类型变量来使用,Java提供了包装类概念。分别定义了它们相应的引用类型
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
在JDK1.5以前,把基本数据类型变成包装类实例,需要通过对应包装类的构造器来实现,8个包装类中,除了Character之外,还可以通过传入一个字符串参数来构建包装类对象。
基本数据类型 – 通过new WrapperClass(primitive)创建 —> 包装类对象
包装类对象 – 通过WrapperClass.xxxValue()方法 —> 基本数据类型
注意:上面的用法已经过时。
自动装箱 和 自动拆箱
JDK 1.5 提供了自动装箱 和 自动拆箱 功能。
自动装箱:就是可以把一个基本数据类变量直接赋值给对应的包装类变量,或者赋值给Object变量(Object是所有类的父类,子类对象可以直接赋值给父类)
自动拆箱:允许把包装类对象直接赋值给一个对象的基本数据类型变量。
当基础类型与它们的包装类有如下几种情况时,编译器会自动帮我们进行装箱或拆箱:
- 赋值操作(装箱或拆箱)
- 进行加减乘除混合运算 (拆箱)
- 进行>、<、==比较运算(拆箱)
- 调用equals进行比较(装箱)
- ArrayList、HashMap等集合类添加基础类型数据时(装箱)
Integer x = 1; // 装箱 调⽤ Integer.valueOf(1)
int y = x; // 拆箱 调⽤了 X.intValue()
注意:进行自动装箱和自动拆箱时必须注意类型匹配。
public static void main(String[] args) {
// 直接将基本数据类型变量赋值给包装类变量
Integer i = 5;
Object obj = true;
// 直接将包装类对象赋值给基本数据类型变量
int it = i;
if (obj instanceof Boolean) {
// 先强制类型转换,再赋值
boolean b = (Boolean) obj;
System.out.println(b);
}
}
包装类可以实现基本数据类型和字符串之间的转换:
- 基本数据类型 – 通过String.valueOf(primitive)转换 —> String对象 (另一种方法:把基本数据类型变量和””进行连接运算,系统会自动把基本类型变量装换成字符串)
- String对象 — 通过 WrapperClass.parseXxx()方法或利用包装类Xxx(String s)的构造器 —-> 基本数据类型
public static void main(String[] args) {
String intStr = "123";
// 将字符串转换成基本数据类型变量
int i = Integer.parseInt(intStr);
int i2 = new Integer(intStr);
System.out.println(i2);
String floatStr = "3.1415";
float f = Float.parseFloat(floatStr);
float f2 = new Float(floatStr);
System.out.println(f2);
// 将基本数据类型变量转化成字符串
String s = String.valueOf(f2);
System.out.println(s);
Integer a = new Integer(4);
// 输出 true
System.out.println(a > 3.0);
}
注意:虽然包装类型的变量是引用类型,但包装类的实例可以与数值类型的值进行比较,取出包装类实例的值来比较。
特例:
两个int类型的数组自动装箱成Integer实例,如果它的范围在-128~127之间,永远是引用cache数组中的同一个数组元素,所以相等;
如果不在-128~127之间的整数自动封装成Integer实例,系统总是重新创建一个Integer实例,因此它们是不相等的。
public static void main(String[] args) {
Integer a = -1;
Integer b = -1;
// 输出 true
System.out.println("a和b自动装箱后是否相等: " + (a == b));
Integer e = 128;
Integer f = 128;
// 输出 false
System.out.println("e和f自动装箱后是否相等: " + (e == f));
Integer c = new Integer(2);
Integer d = new Integer(2);
// 包装类的实例实际上是引用类型,只有两个指向同一个对象才是true
// 输出 false
System.out.println("c和d包装类的实例是否相等:" + (c == d));
}
JDK1.7为所有的包装类提供了一个静态的compare(xxx val1, xxx val2)方法,来比较两个基本类型值的大小。
为什么第二个输出是false?看看 Integer 类的源码就知道啦。
IntegerCache静态代码块中的一段,默认Integer cache的下限是-128,上限默认127。当赋值100给Integer时,刚好在这个范围内,所以从cache中取对应的Integer并返回,所以二次返回的是同一个对象,所以比较是相等的,当赋值200给Integer时,不在cache 的范围内,所以会new Integer并返回,当然比较的结果是不相等的。
处理对象
Java对象都是Object类的实例,都可直接调用该类中的方法,这些方法提供了处理Java对象的通用方法。
toString()
Object类提供的toString() 方法总是返回该对象实现类的 ”类名 + @hashCode“ 值。hashCode是8位十六进制数字。
如果用户需要自定义类能“自我描述”的功能,就必须重写Object类的toString()方法。
== 和 equals方法
Java程序中测试两个变量时否相等有两种方法: == 和 equals。
==判断
当使用 == 来判断两个变量是否相等时,
- 如果两个变量是基本类型变量,且都是数字类型(不一定要求数据类型严格相同),则只要两个变量的值相同,就将返回true。
- 对于两个引用类型变量,只有它们指向同一个对象时,== 判断才会返回true。
== 不可用于比较类型上没有父子关系的两个变量。
public static void main(String[] args) {
int i = 65;
float f = 65.0f;
// true
System.out.println(i == f);
char c = 'A';
// true
System.out.println(i == c);
String s1 = new String("Test");
String s2 = new String("Test");
// false
System.out.println(s1 == s2);
// true
System.out.println(s1.equals(s2));
}
需要注意 “Test”和new String(“Test”)是有区别的?
“Test”是字符串直接量(即可以在编译时就计算出来的字符串值),JVM使用常量池来管理这些字符串,
当使用new String(“Test”)时,JVM先使用常量池来管理“Test”直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。也就是说new String(“Test”)一共创建了两个字符串对象。
常量池(constant pool)
专门用来管理在编译时被确定并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口中的常量,还包括字符串常量。
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "H";
String s3 = "ello";
// 不能在编译时确定
String s4 = s2 + s3;
// 在编译时就确定下来了,直接引用常量池中的“Hello”
String s5 = "H" + "ello";
// 引用内存中新创建的String对象
String s6 = new String("Hello");
// true
System.out.println(s1 == s5);
// false
System.out.println(s1 == s4);
// false
System.out.println(s1 == s6);
}
equals判断
equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。
equals()方法是Object类提供的一个实例方法,因此所有的引用变量都可以调用该方法来判断是否与其他引用变量相等。
判断两个对象相等时,和==运算符没有区别,同样要求两个引用变量指向同一个对象才返回true。因此Object提供的equals方法没有太大的意义,可以重写equals()方法实现。
注意,String已经重写了equals()方法,只要两个字符串所包含的字符串序列相同,则返回true。
笼统的说equals()方法是判断两个对象的值相等,这样的说法并不准确。
public class EqualsTest {
public static void main(String[] args) {
Person p1 = new Person("小王", "1234");
Person p2 = new Person("王二", "1234");
Person p3 = new Person("小王", "12345");
// true
System.out.println(p1.equals(p2));
// false
System.out.println(p1.equals(p3));
}
}
class Person {
private String name;
private String idStr;
public Person() {
}
public Person(String name, String idStr) {
this.name = name;
this.idStr = idStr;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getIdStr() {
return idStr;
}
public void setIdStr(String idStr) {
this.idStr = idStr;
}
// 重写equals方法,自定义两个对象相等的条件
public boolean equals(Object obj) {
// 如果两个对象是同一个对象
if (this == obj) {
return true;
}
// 只有当obj对象是Person对象
if (obj != null && obj.getClass() == Person.class) {
Person personObj = (Person) obj;
// 并且当前对象的idStr和obj对象的idStr相等时,才判断两个对象相等
if (this.getIdStr().equals(personObj.getIdStr())) {
return true;
}
}
return false;
}
}
正确的重写equals()方法,应该满足下列条件:
-
自反性:对任意的x,x.equals(x)一定返回true。
-
对称性:对任意x, y,如果x.equals(y)返回true,则y.equals(x)返回true。
-
传递性:对任意x, y, z,如果x.equals(y)返回true,y.equals(x)返回true,则x.equals(z)也返回true。
-
一致性:对任意x, y,如果对象中用于等价比较的信息没有改变,那么无论x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,一直是false。
-
对任何不是null的x, x.equals(null)一定返回false。
Object默认提供的equals()只是比较对象的地址,即Object类的equals()方法比较的结果与==运算符比较的结果完全相同。因此,在实际应用中常常需要重写equals()方法,相等的条件是由业务要求决定,因此equals()方法的实现也是由业务要求决定。
总结 :== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
使用String a = “abc” 还是new string(“abc”)?
-
String a = “abc”;
首先会在栈中创建一个对String类对象的引用变量a,然后去查找字符串常量池中是否有”abc”。如果有,会把a指向这个对象的地址。如果字符串常量池中没有”abc”,则在栈中创建三个char型的值’a’,‘b’,‘c’,然后在堆中创建一个String对象object,它的值是刚才在栈中创建的三个char型值组成的数组{‘a’,‘b’,‘c’},接着这个String对象object会被存放进字符串常量池中,最后将a指向这个对象的的地址。 -
String b = new String(“abc”);
可以分为两步,String object = “abc”; 和 String b = new String(object);
第一步参照上面,第二步由于”abc”已经被创建并保存在字符串常量池中,因此jvm只会在堆中创建一个String对象,它的值共享栈中已有的三个char型值。
第一种方式JVM会根据String pool中的具体情况来决定是否创建新的对象。
第二种方式一概在堆中创建新的对象,而不管字符串常量池中是否已有这个的字符串。
- 一些创建对象的问题
-
String s = “a” +“b” + “c” + “d”;这条语句创建了几个对象?
创建了一个对象,因为相对于字符串常量相加的表达式,编译器会在编译期间进行优化,直接将其编译成常量相加的结果。 -
String s;
没有创建对象。 -
String a = “abc”;
String b = “abc”;
创建了一个对象,只是在第一条语句中创建了一个对象,a和b都指向相同的对象”abc”,引用不是对象。
类成员
在Java类里只能包含 成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)
5种成员。
static关键字修饰的成员就是类成员。
其中static可以修饰 成员变量、方法、初始化块、内部类(包括接口、枚举),以static修饰的成员就是类成员
。
类变量既可以通过类来访问,也可以通过类的对象来访问。当使用类的对象来访问类变量时,系统会在底层转换为通过该类来访问类变量。
final修饰符
final关键字可以修改 类、变量、方法,final关键字有点类似C#里面的sealed关键字,用于表示它修饰的类、方法和变量不可改变。
final成员变量
final修饰的成员变量必须由程序员显式地指定初始值。
final修饰的类变量、实例变量能指定初始值的地方如下:
- 类变量:必须在静态初始化中指定初始值 或 声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
- 实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能在三个地方的其中之一指定。
final局部变量
系统不会对局部变量进行初始化,必须由我们显式初始化。因此使用final修饰局部变量,既可以在定义时指定初始值,也可以不指定初始值。
如果在定义时没有定义初始值,则在后面的代码中可以对final变量赋初始值,但只能一次,不能重复赋值。
如果final修饰的的局部变量在定义时已经定义初始值,则后面代码不可以进行赋值。
final修饰基本数据类型变量和引用类型变量的区别
- 当使用final修饰基本数据类型变量时,不能对基本数据类型变量重新赋值,因此基本数据类型变量不能被改变。
- 当对于引用变量时,它保存的是一个引用,final只能保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
可执行“宏替换”的final变量
对于final变量,不管是类变量、实例变量,还是局部变量,只要改变量满足以下三个条件,这个final变量就不再是一个变量,而是相当于一个直接量:
- 使用final修饰符修饰
- 在定义该final变量时指定了初始值
- 该初始值可以在编译时就被确定下来。
综上,该变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
还有一种情况,如果被赋值的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量、调用方法,Java编译器同样会将这种final变量当成“宏变量”处理。
例如: final int a = 5 + 2; final String s = “你” + “好”;
注意: 对于final实例变量而言,只有在定义该变量时指定初始化值才会有”宏变量”的效果。
final方法
final修饰的方法不可被重写。但是还是可以被重载的。
例如,Java提供的Object类中的getClass()方法,是final方法,就不可以被重写。
final类
final修饰的类不可以有子类。例如java.lang.Math类。
不可变类
指创建该类的实例后,该实例的实例变量是不可改变的。 不可变(immutable)
Java提供的8个包装类和java.lang.String类都是不可变类,它们的实例的实例变量不可改变。
如果需要创建自定义的不可变类,可遵循如下规则:
- 使用private和final修饰符来修饰该类的成员变量
- 提供带参的构造器,用来根据传入参数来初始化类里的成员变量
- 仅为该类里的成员变量提供getter方法,不要提供setter方法,因为普通方法无法修改final修饰的成员变量。
- 如果有必要,重写Object类的hasCode()和equals()方法。equal()方法根据关键成员变量来作为两个对象是否相等的标准,此外,还应该保证两个用equals()方法判断是否相等的对象的hashCode()也相等。
缓存实例的不可变
不可变类的实例状态不可改变,可以很方便的被多个对象所共享。如果程序进程使用相同的不可变类实例,可以考虑缓存这种不可变类的实例。
final, finally和finalize的区别
final 用于声明属性,方法和类,分别表示属性不可修改,方法不可重写(override),类不可继承。
final—修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。
将变量或方法声明为final,可以保证它们在使用中不被改变。
被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。
被声明为final的方法也同样只能使用,不能重写。
finally:则是Java保证重点代码一定要被执行的一种机制。在异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
需要关闭的连接等资源,更推荐使用Java 7中添加的try-with-resources语句,因为通常Java平台能够更好地处理异常情况,编码量也
要少很多。
finalize—方法名。它是在 Object 类中定义的,因此所有的类都继承了它。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。子类可以覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。fnalize机制现在已经不推荐使用,并且在JDK 9开始被标记为deprecated。
内部类
类是一个独立的单元,在某些情况下,会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就叫做内部类。(也叫作嵌套类)。包含内部类的类也被称为外部类。
从JDK1.1开始引入。
内部类的作用:
- 提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
- 内部类成员可以访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以相互访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
- 匿名内部类适合用于创建那些仅需要一次使用的类。
从语法角度,定义内部类和外部类大致相同,内部类除了定义在其他类内部,还有以下不同:
- 内部类比外部类可以多使用三个修饰符:private、protected、static (外部类不可以使用这三个修饰符)。
- 非静态内部类不能拥有静态成员。
编译具有一个内部类的java文件,会产生多个class文件(注意内部类和外部类的数量);成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形式:OuterClass$InnerClasss.class。
大部分时候,内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;
局部内部类和匿名内部类则不是类成员。
成员内部类分为两种:静态内部类和非静态内部类。使用static修饰的成员内部类是静态内部类,反之,则为非静态内部类。
非静态内部类
当在非静态内部类的方法内查找某个变量时,顺序如下:
- 该方法内查找是否存在该名字的局部变量
- 在该方法所处的内部类中查找存在该名字的成员变量
- 在该内部类所处的外部类中查找存在该名字的成员变量
- 如果不存在,报编译错误
如果外部类成员变量、内部类成员变量、内部类方法里的局部变量同名,则可以通过使用this、外部类列名.this 作为限定来区分。
非静态内部类的成员可以访问外部类的private成员,但反过来就不成立。
如果外部类想要访问非静态内部类的成员,则必须显示创建非静态内部类对象,来调用访问其实例变量。
非静态内部类里不能有静态方法、静态成员变量、静态初始化块,有就会引起编译错误。
静态内部类
用static修饰的内部类,称为类内部类
,或者静态内部类
。这个类属于外部类本身,不属于外部类的某个对象。。
静态内部类不能访问外部类的实例变量,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员。
外部类不能直接访问内部类的成员,但可以使用静态内部类的类名作为调用者访问静态内部类的类成员;使用静态内部类对象作为调用者来访问静态内部类的实例成员。
局部内部类
放在方法里定义的内部类,叫做局部内部类。仅在方法里有效。
注意:
- 所有的局部成员都不能使用static修饰
- 所有的局部成员都不能使用访问控制符修饰
匿名内部类
没有名字的局部内部类。如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类。
语法格式:
new 实现接口() | 父类构造器(实参列表)
{
// 匿名内部类的类体部分
}
注意,
- 匿名内部类不能是抽象类。
- 匿名内部类不能定义构造器。但是可以定义初始化块,用来完成构造器需要完成的事情。
public class InnerClassTest {
public static void main(String[] args) {
int age = 8;
// 匿名内部类
A a = new A() {
@Override
public void test() {
System.out.println(age);
}
};
a.test();
}
}
interface A {
void test();
}
使用JDK 1.8编译上面的程序,就正常。如果用Java 8以前的版本,就会有编译错误。
Java 8之前,Java要求被局部内部类、匿名内部类访问的局部变量,必须使用final修饰。从Java8开始,这一限制被取消,Java8更加智能:如果局部变量被匿名内部类访问,那么该局部变量相当于使用了final修饰。
当然,也可以使用final修饰,也可以不使用final修饰,但必须按有final修饰的方式来用:
也就是一次赋值后,以后不能重复赋值。
如果重复赋值,就会报下面的错误:
枚举类
Java从JDK1.5后增加了对枚举类的支持,新增了enum关键字(它与class、interface关键字的地位相同)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/155834.html