Java基础语法
Java规范
image.pngJava字节码
java程序是以".java"为扩展名,当我们编写完java程序后,要执行程序需要经过两个阶段:编译和运行。
编译器
执行编译程序的称为编译器,java将java源文件编译成为字节码(bytecode)。字节码是高度优化的指令集合,但是字节码并不能直接被计算机所执行,这些指令只有java运行时系统执行(又称Java虚拟机,Java Vitual Machine)。
在Java中,源文件正式名称是编译单元(compilation unit),源文件是包含一个或多个类的定义的文本文件,文本文件以.java作为扩展名。源文件中的主类需要与源文件名称一致,即一个源文件有且只有一个主类,这有利于维护和组织程序。
将java程序编译成字节码文件,解决了两方面问题:跨平台和安全性。将java程序编译成统一的字节码文件,然后针对于不同的操作系统、硬件去实现Java虚拟机,这样只要给定的系统有运行时包就可以执行java程序,有效的解决了跨平台问题。因为JVM控制程序的执行,所以能够防止程序对所在的系统产生负面作用。
虽然java程序被设计为解释性语言,但是为了提供性能,也可以将字节码编译为本机代码。HotSpot为字节码提供了即时编译器(Just-Time,JIT),如果JVM中包含JIT编译器,就可以将一部分代码实时编译为机器可执行代码。
通过javac命令执行java编译器,会生成HelloWorld.class字节码文件。需要注意,源文件中如果包含多个类,编译后每个类都会生成一个以类名为名称,以.class结尾的字节码文件。
javac HelloWorld.java
解释器
Java编译器生成的字节码文件并不能直接被机器执行,需要通过JVM将字节码文件解释为机器可以识别的代码,然后才可以执行。
通过java命令运行程序,并且指定要执行的主类名称。Java会自动搜索包含该类名称且扩展名为.class的文件,如果找到就会执行指定类中的代码。
java HelloWorld
Java关键特性
java具有简单性、安全性、可移植性、面向对象、健壮性、多线程、体系结构中立、解释执行、高性能、分布式、动态性的特点。
编程范式
程序构造目前有两大范式,即:面向过程编程(process-oriented programming)和面向对象编程(object-oriented programming)。面向过程编程是围绕着“正在发生什么”进行编写,这种方式是将程序描述为一系列线性步骤,是将代码作用于数据之上。随着程序规模的不断增长,面向过程编程难以管理复杂、庞大的程序,这时就发明了第二种范式。面向对象编程是围绕着“将影响谁”,它是围绕着数据以及一系列精心设计的接口组织程序,即数据控制代码的访问。
抽象
面向对象的本质元素之一就是抽象,通过抽象管理复杂。而层次化分类是管理抽象的一种有力方式,将复杂系统的语义进行分层,将他们分解为多个更易于管理的部分。
OOP三原则
面向对象编程提供了用于帮助实现面向对象模型的机制,这些机制就是封装、继承、多态。
注释
Java提供三种注释:单行注释、多行注释、文档注释。
- 单行注释用于注释用于简单描述,以“//”头,并作用于到行尾。
- 多行注释用于更长的注释,以“/*” 开头,并以“*/”结尾。
- 文档注释用于生成说明程序的HTML文件,文档注释以“/**”开头,并以“*/”结束。
代码的组成
java程序是由空白符、标识符、字面值、关键字、运算符、注释和分隔符组成的。
- 空白符:程序中每个标记之间的空白符,可以是空格、制表符、换行符。
- 标识符:用于命名事物,由字母(大小写敏感)、数字、下划线和美元符号组成,不能以数字开头。从JDK 8开始,不建议使用下划线作为标识符了。
- 字面值:字面值就是常亮的值,比如100、1.1、’X’、“hello”等。
- 分隔符:分隔符包含“()”、“{}”、“[]”、“;”、“,”、“.”、“::”。
- 关键字:java目前定义了50多个关键字,这些关键字与运算符和分隔符的语法组成了java语言的基础。关键字是系统用词,不能用作标识符,所以不能用作变量、类名和方法名。
数据类型
Java是一种强类型化语言,即每个变量、每个表达式具有一种类型,每种类型都是严格定义的。并且所有赋值,不管是显示的还是方法调用中通过参数传递的,都要进行类型兼容检查。
基本类型
Java定义了8中基本数据类型:byte、short、int、long、float、double、char、boolean。这8种基本类型可以分为:
- 整型:包括byte、shot、int和long,用于表示有符号整数
- 浮点型:包括float和double,用于表示小数。
- 字符型:包括char,用于表示字符集中的符号,比如字母和数字。
- 布尔型:包括boolean,用于表示true/false的特殊类型。
尽管在java中一切皆对象,但是基本类型并不是对象,这样设计的原因是效率,如果将其设计为对象会极大地降低性能。这些基本类型在java中是有明确的范围的(c和c++会随着执行环境不同,其范围也不同),因为java需要具备可移植性。
整数类型
java中提供了四种整数类型:byte、short、int和long,这些类型都有符号的,java不支持无符号的、只是正值的整数。
名称 | 字节(byte) | 宽度(位) | 范围 |
---|---|---|---|
byte | 1 | 8 | -128 ~ 127 |
shot | 2 | 16 | -32 768 ~ 32 767 |
int | 4 | 32 | -2 147 483 648 ~ 2 147 483 648 |
long | 8 | 64 | -9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807 |
所有的整数值都是整型的字面值,字面值可以以二进制(JDK7开始支持)、八进制、十六进制表示。
int v1 = 123_456_789; //相当于x=123456789
int v2 = 0b10__0100__1010; //二进制
int v3 = 0735; //八进制
int v4 = 10; //十进制
int v5 = 0XF25; //十六进制
从JDK7开始,可以使用二进制指定整型字面值,使用0b或0B作为数值前缀。
从JDK7开始,整形字面值可以嵌入一个或多个下划线,嵌入下划线可以使阅读很大的整数时更容易。下划线只能用于分隔数字,不能位于字面值得开头和结尾,并且可以使用连续的多个下划线。
浮点类型
浮点数也称为实数,当计算需要小数精度的表达式时使用。java实现了IEEE-754标准集的浮点类型和运算符。float为单精度类型,double为双精度类型。
名称 | 字节 | 宽度(位) | 大致范围 |
---|---|---|---|
float | 4 | 32 | 1.4e-045 ~ 3.4e+038 |
double | 8 | 64 | 4.9e-324 ~ 1.8e+308 |
浮点字面值可以使用标准计数法或科学计数法表示,如2.32、2.3E5、3.14e-3,E或e表示10的幂。java中浮点字面值默认为double,如果要制定为float,需要在常量后面添加F或f。
字符类型
用于存储字符集的类型是char,java所表示的字符集是Unicode字符集。Unicode定义了一个完全国际化的字符集,Unicode是数十种字符集的统一体,能够表示人类语言中的所有字符(至少目前是)。Unicode需要16位宽,所以char类型也需要16位宽。
ASCII标准字符集的范围是0~127,扩展的ISO-Latin-1范围是0~255。
名称 | 字节 | 宽度(位) | 大致范围 |
---|---|---|---|
char | 2 | 16 | 0 ~ 65536 |
对于char类型既可以直接赋值字符,也可以赋值字符所在Unicode中的所在位置。
char a = 88; //代表'X'
char b = 'Y';
在java正式规范中,char被当做整数类型。然而在实际应用中,主要将其用来表示Unicode字符,所以将其单独分为一类。
字符类型字面值可以使用位于一对单引号中的字符表示,对于那些不可见的字符可以使用转义字符。也可以使用八进制或十六进制表示,对于八进制使用反斜杠后加三位数字表示,对于十六进制以 "\u"后加4位十六进制数,比如 '\121'、'\ua432'。
char v7 = 22;
char v8 = 'a';
char v9 = '\121';
char v10 = '\ua245';
转义序列 | 描述 |
---|---|
\ddd | 八进制字符(ddd) |
\uxxxx | 十六进制Unicode序列(xxxx) |
\ ' | 单引号 |
\" | 双引号 |
\\ | 反斜线 |
\r | 回车符 |
\n | 换行符 |
\f | 换页符 |
\t | 制表符 |
\b | 回格符 |
布尔类型
在java中布尔类型用于表示逻辑值,它只能为true或false,所以它只需要一个字节即可。
布尔字面值只有两个逻辑值:true和false。true和false不能转换为任何数字,也就是说 boolean a = 1
是不支持的。
变量
在Java中,变量是基本存储单元。定义变量通过标识符、类型以及可选的初始化器来定义。所有变量都有作用域,作用域定义了变量的可见性和生命周期。
type identifier = [value][,indentifier [= value] ...];
变量可以在定义的时候初始化,也可以动态的初始化。
double v11 = 1.1;
double v12;
for(int i=0; i<10 ;i++) {
v12 = Math.sqrt( i * i );
}
变量作用域
Java允许在任何代码块中定义变量,代码块定义了变量的作用域,每个变量的作用域都是在代码块中的。作用域决定了变量的可见性和生存周期。
尽管java通过代码块来划分作用域,在实际使用中人们经常将变量定义在类中的称为类变量或全局变量,定义在方法中的称为方法变量或局部变量。
类变量
类变量定义在类体内部,方法体外部。类变量可以定义在类中的任何位置,整个类都可以使用。类变量可以不初始化值直接使用,这样会根据数据类型使用类型默认值。
方法变量
方法中定义的变量和方法内代码块定义的变量具有相同特性,它们的作用域都是从定义位置开始到代码块结束(和类变量不同),都需要先初始化在使用。
public static void main(String[] args){
double v11;
//double v13 = v11 + 1; //需要先初始化
for(int i=0; i<10 ;i++) {
double v12 = Math.sqrt( i * i );
}
//System.out.println(v12); //v12的作用域在for循环内
}
注意:方法中定义的变量,内层代码块中不允许与外层代码块定义相同的变量名称
类型转换
Java类型转换分为自动类型转换和强制类型转换,例如int类型总能自动转换为long类型,但是double类型却不能自动转换为int类型,所以需要强制类型转换(cast)。
自动类型转换
自动类型转换也成扩展转换(widening conversion),只要满足以下两个条件就会发生自动转换。
- 两种类型是兼容的
- 目标类型大于源类型
对于自动扩展类型,数值类型(整数和浮点型)是相互兼容的,但是数值类型不能和char或boolean类型的自动转换,char和boolean相互也是不兼容的。
强制类型转换
对于相互之间不兼容的类型,或目标类型小于源类型,就需要使用强制类型转换了。强制类型也称为缩小转换(narrowing conversion),因为显示的将数值变的更小。
//(target-type) value; 强制类型转换
int v14 = 257;
byte v15 = (byte) v14;
对于整数类型,当进行强制类型转换后,如果源类型超出了目标类型,将会以数值除以目标类型范围取模。比如,上面将int类型强制转换为byte类型,并且超过了byte类型范围,则 v14/256(byte的范围),结果为1。
对于浮点类型,如果将浮点类型转换为整数类型时,则会进行截尾,即把小数点截掉。
表达式中自动类型提升
除了赋值外,在使用表达式时也可能发生某些类型转换。
- 表达式中byte、short、char会自动提升为int类型。
- 如果表达式中有一个操作数是long型,则整个表达式结果会提升为long型。
- 如果表达式中有一个操作数是float型,则整个表达式结果为float类型。
- 如果表达式中有一个操作数是double型,则整个表达式结果为double类型。
有时我们会想如果我们变量本身就在byte范围就把他定义为byte类型,这样可以节省空间、提升效率。但其实这是错误的,因为在表达式中byte、char、short需要转换为int类型。
自动类型提升,有时会引起难以理解的编译错误,比如下面的例子:
byte b = 50;
b = b * 2;
会提示编译错误,因为b * 2会提升为int类型,但是int类型无法自动转换为byte类型,所以需要强制类型转换,b = (byte) b * 2。
数组
数组即一组类型相同的变量,可以创建任意类型、任意维(一维或多维)的数组,访问数组元素是通过数组下标索引来访问的。
创建数组:
//定义规范
type var-name[] = new type[n];
type[] var-name = new type[n];
int[] v16 = new int[10];
int v17[] = new int[19];
如果只声明数组,数组实际并没有创建物理存储地址,需要使用new关键词为数组指定数组长度。数组定义完成后,会为其设置默认值,对于数值类型会被自动初始化为0、布尔类型会被初始化为false、引用类型会被初始化为null。
在定义数组时候,可以直接为数组初始化:
int[] v18 = {1,2,3};
在Java中多维数组实际上就是数组的数组,java允许你只为第一维指定长度,其它维度长度可以自行指定,所以长度可以不同。
int[][] v19 = new int[10][10];
int[][] v20 = new int[10][];
v20[0] = new int[1];
v20[1] = new int[2];
v20[3] = new int[3];
运算符
Java提供了丰富的运算符,可以将其分为4组:算术运算符、关系运算符、逻辑运算符和位运算符。另外还有比较特殊的类型比较运算符instanceof和箭头运算符->。
算术运算符
算术运算符的操作数必须是数值类型,不可以为boolean类型使用算术运算符,但是可以为char类型使用算术运算符,因为char的本质是int的子集。
算术运算符包括:+、-、*、/、%、+=、-=、*=、/=、%=、++、--
。其中+=这类叫做赋值复合运算符,a++
相当于a = a + 1
,复合运算符不仅使用方便,而且运行效率也高。
使用除法运算符,对于整数其结果不会包含小数。
对于自增运算符++、--,如果单独使用a++和++a没有区别,但是如果放到表达式中c = a++和c = ++a是不同的,c = a++ 会现将a赋值给c之后在进行自增,而c = ++a是将a+1执行之后再赋值给c。
关系运算符
关系运算符用来判断一个操作数与另一个操作数之间的关系,关系运算符的结果为布尔值。
关系运算符包括:==、!=、>、>=、<、<=。
在java中true和false不是数值,它们与是否为0没有任何关系(c/c++中true代表任何非零值,false是0),所以下面的语句是不对的:
int a = 1;
if(a)
而应该为:
if(a == 1)
逻辑运算符
逻辑运算符只能操作布尔类型操作数。
逻辑运算符包括:&、|、^、||、&&、!、&=、|=、^=、==、!=、?:。
“?:”是Java提供的特殊三元运算符,它可以替代“if-then-else”语句。
expression1 ? expression2 : expression3
当expression1为true的时候,就对expression2求值;否则就对expression3求值。比如:</br>
a = b == 0 ? 1 : 2;
注意&&和||又称为短路逻辑运算符,对于a&&b,如果a为false时则b就不会被执行;对于a || b,当a为true时,则b不会被计算,因为结果已经为true了。
如果不想使用短路运算的特点,可以使用单符号&和|来进行与、或运算。
位运算符
位运算符可以用于整数类型char、shot、int、long和byte。
因为位运算符是对整数进行操作的,所以我们需要先了解一下整数是如何存储以及如何表示负数的。
Java中所有整数都有由宽度可变的二进制数字表示的,例如byte类型的42为00101010,从右到左代表2的n次幂(从0开始),所以42 = 2¹ + 2³ + 2^5 。
所有整数类型(除char)都是有符号整数,既可以是正数,也可以是负数。Java使用“2的补码”进行编码,也就是说负数的表现方法为:首先翻转所有位(1变为0,0变为1),然后再将结果加1。比如-42,反转11010101,然后在加1为11010110。如果想要将-42在转换为42,也需要反转所有位,然后在加1。
逻辑运算符包括:
运算符 | 描述 | 使用 | |
---|---|---|---|
~ | 按位取反 | 00101010 取反 11010101 | |
& | 按位与,只有两个操作数都为1,结果才为1 | 0011 & 1010 = 0010 | |
| | 按位或,只要两个操作数有一个为0,结果就为0 | 0011 | 1010 = 1011 |
^ | 按位异或,两个操作数只有一个操作为1,结果就为1 | 0011 ^ 1010 = 1001 | |
>> | 右移,将数值中所有位向右移动指定的次数,左侧补0(不一致为0,填充符号位)。右移一位相当于除2 | 64 >> 2 = 16 | |
<< | 左移,将数值中所有位向左移动指定次数,右侧补0(不一致为0,填充符号位),注意如果移到符号位,有可能更改正负数。左移一位相当于乘2 | 64 <<2 = 256 | |
>>> | 右移零填充,又称为无符号右移,即不会根据符号位填充左侧的值,一直都是0 | a = -1 </br> a = a >>> 24 11111111 11111111 11111111 11111111 >>> 24 = 00000000 000000000 000000000 11111111 |
|
&= | 按位与并赋值 | a = a&2 | |
|= | 按位或并赋值 | a = a|2 | |
^= | 按位异或并赋值 | a= a^2 | |
>>= | 右移并赋值 | a = a >> 1 | |
<<= | 左移并赋值 | a = a<< 1 | |
>>>= | 右移零填充并赋值 | a = a>>> 1 |
通过左右移位可以实现高效的乘除2操作。
当进行右移时右移后的顶部(最左边)位使用右移前的最高位(最左边的第一位,符号位)进行填充,这称为符号扩展,该特性能够保留负数的符号。比如:
11111000 -8
-8 >> 1
11111100 -4
对于-1无论怎么右移,最后的结果一直都是-1。
还需要记住,对于byte、short都会自动提升为int类型。
赋值运算符
赋值运算符使用单个"="号,
形式:
var = expression
赋值运算符允许创建赋值链,将一组变量设置相同值。比如 x = y = z = 10
运算符优先级
表达式中的运算符优先级影响着其表达式的结果,Java运算符优先级从高到低:
自增运算符(++、--)
算术运算符(*、/、%、+、-)
移位运算符(>>、>>>、<<)
关系运算符(>、>=、<、<=、instanceof)
等值运算符(==、!=)
逻辑运算符(&、^、|、&&、||)
三元运算符(?:)
箭头运算符(->)
赋值运算符(=、op=)
在使用中不需要牢记这些,通过括号可以轻松控制优先级,并且帮助代码理解,而且使用括号不会影响效率。
控制语句
Java的程序控制语句分为:选择语句、迭代语句和跳转语句。
选择语句
Java支持两种选择语句:if语句和switch语句。
if语句
if语句是Java的条件分支语句,可以使用if语句控制程序执行不同的路径。
if语句语法:
//if-else
if(condition) {
statement1;
}else {
statement2;
}
//if-else-if 从上向下执行,一旦某个条件为true,就会执行与之关联if语句,并且会忽略剩余的语句
if(condition) {
statement;
}else if(condition) {
statement;
}
.
.
.
else {
statement;
}
switch语句
switch语句是Java的多分支语句,根据表达式的值调度执行代码的不同部分,switch语句是if-else-if语句更好的替代方法。
switch语句语法:
switch(expression) {
case value1:
statement;
break;
case value2:
statement;
break;
.
.
.
case valueN:
statement;
break;
defalut:
statement;
}
expression表达式在JDK7之前可以是byte、short、int、char或枚举类型,在JDK7开始允许为String类型。case后指定的每个数值必须是唯一的常量表达式,不允许重复,并且类型需要和expression的类型兼容。可以通过break语句终止后面的执行,跳出switch语句。
注意:statement可以为多行语句,不需要使用{}代码块。
sitch语句有三个重要的特征:
- switch语句只能进行相等性比较,即expression与case常量相匹配。
- 在同一switch语句中,不允许具有相同的case常量。对于嵌套switch语句,内层与外层可以具有相同的case常量。
- 相对于一系列嵌套的if语句,switch语句更加高效,因为编译器本身知道所有case常量具有相同的类型,并且只进行相等性比较。
迭代语句
Java迭代语句包括for、while和do-while语句,这些语句又称为循环语句。
while语句
只要控制表达式为true,while循环就重复执行一条语句或代码块。while循环的循环体可以为空,在Java中空语句的语法是合法的(只包含一个分号的语句)。
while语句语法:
while(condition) {
//body of loop
}
do-while语句
有时我们需要至少执行一次循环,无论条件表达式是真还是false,即希望在循环末端终止循环。可以使用do-while语句:
do{
//body of loop
} while(condition)
for循环
for循环是一种通用的、强大的循环结构,从JKD1.5开始就已经有两种形式了。
传统for循环
传统for循环是以初始条件(循环控制变量)、布尔表达式(测试循环控制变量)和迭代器(循环控制变量自增或自减)组成。第一次循环的时候执行initialization,初始化循环变量(只会执行一次)。然后判断condition,如果为true则执行循环体,如果为false就停止执行。接下来的每次循环都先执行一次iterator,然后判断condition来决定是否执行循环体。
for(initialization; condition; iterator){
//body
}
for循环可以灵活运用,下面是一些常用方式。
- 在for循环内部声明循环变量。
for(int i=0; i<n; i++) {
}
- 多变量使用逗号分隔。
for(int a=0,b=10; a<b; a++,b--) {
}
- 省略初始化部分和迭代部分
for( ; !done; ){
}
- 无限循环
for( ; ; ){
}
增强for循环for-each
for-each风格的循环被设计为以严格的顺序(传统for循环是通过使用循环控制变量,手动索引数组的)、从头到尾循环变量一个对象集合,比如数组、集合等。
for(type itr-val : collection){
}
collection指定了需要对什么集合进行遍历,itr-val为每次循环接受集合中元素的变量。type类型需要为collection元素的类型,因为itr-val是用来接受集合元素的。
需要注意迭代变量是“只读”的,即不能通过更改迭代变量,来更改集合的内部元素。
跳转语句
java支持三种跳转语句:break、continue、return。异常机制其实也可以算成一种跳转语句,通过抛出异常来更改当前执行分支。
break语句
break语句有三种用途:用于终止switch语句、用于退出循环、goto语句“文明”使用。
- 对于退出循环需要注意的是,当有多层循环时break只会终止它所在的那层循环。
- 一个循环中可以出现多个break语句,但是过多break语句会破坏代码结构。
- 在某条switch语句中使用break,只会影响该switch,不会结束任何外层循环。
break可以作为goto语句文明使用,我们知道goto语句可以随意进入任意一个程序分支,是一种非结构化的方法,使用goto语句会使代码难以理解和维护,还会妨碍编译器优化。但是goto语句还是有用武之地的,比如退出深层循环。java提供的break语句,提供了goto的优点、去除了goto语句的缺点。
break label;
label是一个标签,当执行到该语句时,会跳转至标签定义的位置,最常用的标识一个代码块。
first: {
second: {
third: {
System.out.println("Before break");
if( 1== 1)
break second;
System.out.println("inner third");
}
System.out.println("inner second");
}
System.out.println("inner first");
}
//输出:
Before break
inner first
label: for(int i=0; i<10; i++) {
for(int j=0; j<100; j++) {
if(j == 10)
break label;
}
}
注意,break语句只能跳到包含该break上层的label上,对于其它的label是无法跳到的。
continue语句
continue用于提前终止一次迭代,当执行到continue语句时,结束本次循环,开始下次的接待。continue语句也可以指定标签,描述继续执行哪个包含它的循环。
label: for(int i=0; i<10; i++) {
for(int j=0; j<100; j++) {
if(j == 10)
continue label;
}
}
return语句
return语句是显示的从方法返回,return语句将将程序执行控制转移给方法的调用者。
类
类基础
类定义了一种新的数据类型,一旦定义好一个类,就可以使用这种新的类型创建该类型的对象。因此,类是对象的模板,对象是类的实例。
在类中封装了数据以及对数据的操作,即变量和方法,变量和方法都称为类的成员。在类中定义的变量称为实例变量,因为类的每个实例都包含这些变量的副本,每个对象的变量和方法都是独立和唯一的。
//类定义
class className {
type instance-variable;
...
type method(param-list) {
}
...
}
//创建对象
className object = new className(param-list);
object.instance-variable; //访问该对象的实例变量
object.method(param-list); //访问该对象的方法
对象的变量和方法都是类的副本,每个对象的变量和方法都是相互独立,互不影响的。
对象创建
当声明完类后,就创建了一个新的数据类型。如果要使用该类型的对象需要经过两步:声明对象变量和获取对象的实际物理副本并赋值给对象变量。
声明对象变量,并没有定义对象,它只是一个引用对象的变量。要创建对象获取对象的实际物理副本,需要使用new运算符动态的为对象分配内容,并返回该对象的引用,而这个引用就是new为该对象分配的内容地址。然后将这个引用存储在变量中。
对象的引用和c/c++中的指针类似,但是由于java安全机制,不能像指针那样真实的操作引用,也不能为引用随意分配内容地址
我们知道创建基本类型并不需要使用new创建,是因为java的基本类型不是作为对象实现的,而是作为常规变量实现的。这样能够提高效率,因为对象有很多特征和属性,所以它的开销是与基本类型不同的,所以java更高效的实现了基本类型,对于这些基本类型的完整对象java也有提供,比如Integer、Double。
Person p1 = new Person();
Person p2 = p1;
当将一个对象引用的变量赋值给另一个对象的引用的变量时,不会重新创建对象、重新分配内存空间,而是将新的对象引用变量也指向了前一个对象的引用,比如上面的p1和p2都是指向同一个内存空间,修改任何一个对象内容,都会互相影响。
类方法
类中除了定义数据的实例变量外,还有用于操纵数据的方法。方法可以带参数,方法定义时的参数称为形参,用于接受调用方法传递进来的参数变量。当调用方法时传递进来的参数称为实参,实参会赋值给形参。
设计良好的程序,应该通过方法访问类的实例变量,这样可以改变方法的行为,但不会暴露实例变量的行为。
构造函数
构造函数是类方法中的一种特殊方法。当创建对象时,一般都会进行一些初始化操作,而java通过构造函数进行对象自身的初始化,构造函数在创建对象之后立即进行初始化操作(构造函数的目的就是初始化对象的内部状态)。构造函数名称与类名称一致,并且没有返回值(实际返回类型是隐式返回类本身),java支持有参构造函数和无参构造函数。当不显示定义构造函数时,会默认创建一个无参的构造函数,而默认无参的构造函数会进行一些默认的初始化操作:对所有实例变量初始化为默认值(这就是为什么全局变量不需要显示初始化操作),数值类型初始化为0或0.0、引用类型初始化为null、boolean初始化为false。一旦定义自己的构造函数,将不会使用默认的改造函数(但还会进行实例变量初始化)。
this关键词
有时我们需要在类方法中引用调用自身的对象,java提供了this关键词,this总是指向调用该方法的对象引用(this就是当前对象本身)。
String name;
int age;
public Person(String name,int age) {
this.name = name;
this.age = age;
}
方法重载
在java中支持在同一个类中定义两个或多个共享同一个名称的方法,只要他们的参数声明不同即可(参数的个数或参数类型),这个过程称为方法重载(method overloading)。方法重载是Java支持多态性的一种方式。需要注意,方法的返回类型不能作为重载方法选择的条件。
当java遇到方法重载调用时,会对形参和实参进行匹配,找到对应的版本。但是这个匹配不需要精确的,因为Java本身支持自动类型转换,比如当传入int类型的实参时,发现重载方法中没有int类型对应的方法,则它会将类型提升为double类型,寻找double类型的方法。这种自动类型转换,只有当没有找到精确的匹配时才会发生。
方法重载的价值在于允许使用通用名称访问相关方法。虽然允许使用通用名称重载不想关的方法,但是不应该这么做。除了常规方法的重载,也可以重载构造方法。
参数传递
传递参数通常有两种方式:值传递和引用传递。使用值传递是将基本类型值的副本传递过去了,所以对接受的参数修改不会影响外部。但是如果传递的是对象,因为对象是通过引用传递的,所以修改接受到的对象,就会影响原对象。
可变长参数
在JDK5之后Java提供了一种定义可变长参数的方法:
public void methdod(String ... str){
}
用户在调用可变长参数方法时,可以传入0个或多个参数,在方法内部接受可变长参数是通过数组形式接受的,上面实例实际使用一个String数组接受可变长参数。
使用可变长参数有一些需要注意的点:
- 可变长参数只能在方法参数列表中最后声明。
- 一个方法只能有一个可变长参数。
- 可变长参数可以进行重载,但需要避免模糊性调用。
访问控制
Java提供的访问控制包括:private、protected、默认访问和public。
private
如果类的成员变量或方法使用private修饰,则该变量或方法只能被包含这些方法和变量的类所访问。
默认访问控制权限
如果类的成员变量或方法采用默认访问控制,则该变量或方法在包内是共有的,能够被包内其它类所访问。
protected
如果类的成员变量或方法使用protected修饰,则该变量或方法在包内时共有的,能够被包内其它类所访问。除了能被包内访问,也能被子类所访问,即便子类没有在该包内。
public
如果类的成员变量或方法使用public修饰,则该方法或变量可以被任何代码访问。
main()方法之所以使用public修饰,是因为main()方法需要被Java运行时系统访问。
良好的程序设计,不应该能够直接操作变量,而是通过方法操作。
static关键字
使用static关键字修改类的成员变量或方法,则这些变量和方法称为静态变量和静态方法。静态变量和静态方法可以直接使用类名访问,而不需要创建任何对象。静态变量和静态方法是被所有对象所共享的(普通变量或方法,每创建一个对象就会为其创建一个副本)。
静态变量也可以在定义时不赋值,可以通过静态代码块进行赋值。静态代码块只执行一次,当第一次加载该类的时候执行。
static {
...
}
静态方法有几个限制:
- 静态方法中,只能访问静态变量。
- 静态方法中,只能调用静态方法。
- 静态方法中,不能使用this、super关键字(因为和对象有关)。
普通方法可以调用静态变量和方法(静态变量和方法被所有对象共享),但是静态变量和方法不能被对象所调用
main()方法就是静态访问,因为JVM需要在创建所有对象之前调用该方法。
final关键字
final关键字一般用来声明常量使用,被final声明的变量就不能够再被重新赋值了,这也意味着在声明final变量时需要为其进行初始化。可以通过两种方式为其赋值:第一种就是声明时赋值;第二种就是在构造函数中为其赋值。
方法参数和局部变量也可以声明为final。为参数声明final,可以防止方法中修改参数。为局部变量声明为final,可以防止其被多次赋值。
final关键字也可以用于修饰方法,被final修饰过的方法,是不能被子类所重写的。
final关键字也可以用于修饰类定义,被final修饰过的类,是不能被子类所继承的。
嵌套类和内部类
我们可以在类的内部定义另一个类,这种类被称为嵌套类。嵌套类的作用域被限制在包含它的类中,也就是说我们只能在包含它的类中使用嵌套类,在外部是无法使用嵌套类的。
嵌套类可以访问包含它的类的成员变量和方法(包括私有成员变量),但是包含类不能访问嵌套类的成员(和局部变量类似)。
嵌套类有两种类型:静态嵌套类和非静态嵌套类。静态嵌套类使用static关键字修饰,但是静态类不能直接访问包含类的非静态成员,只能通过创建包含类的对象来访问,正是由于这个限制,静态嵌套类才很少使用。
public class A {
int a = 0;
static class B{
public void print(){
A a = new A();
System.out.println(a.a);//只能通过创建外部类对象,来访问外部类非静态成员
}
}
}
非静态嵌套类是嵌套类的主要类型,被称为内部类。内部类可以访问包含类的所有变量和方法。
public class A {
int a = 0;
class B{
public void print(){
System.out.println(a);//直接使用外部类成员
}
}
}
只能在包含了内部类的类中创建内部类对象,如果在其他类中创建,会直接编译失败。
内部类可以在任何代码块中定义,无论方法中或者循环语句中,都可以定义内部类。
内部类是JDK1.1之后才提供的。
继承
继承是面向对象编程(OOP)的基石之一,通过继承可以创建层次化分类。使用继承可以创建具有共性的超类,然后子类通过继承超类来获取共性特征,子类自身再去实现自身的特性。
只需要使用extends关键字,就可以继承另一个类。
public class A{
private int a;
int b;
}
public class B extends A{
int c = 2;
public void show(){
System.out.println("b:"+b);//不能访问a,因为是父类私有变量
}
}
子类包含了父类所有的成员,但是子类不能访问超类的私有成员。因为超类本身也是一个独立的、单独的类,也有自己的所有成员。
使用protected声明的变量或方法,不仅能够被包内的其它类访问,也可以被子类所访问(即便子类没有在该包中)。protected与默认访问权限,区别就在这里,能够被子类所访问。
可以将子类对象的引用赋值给超类,但是需要注意这时候该变量只能访问子类对象在超类中定义的部分(也就是只能访问超类中的东西)。因为可以访问哪些变量,是由引用类型决定的,而不是由所引用对象的类型决定的。其实这样是合理的,因为对于超类来说,它并不知道子类添加了什么内容。
A ref = new B();//将子类引用对象赋值给父类变量
ref.b;
ref.c;//错误,不能访问子类内容
子类只能继承一个超类,Java不支持多重继承。
super关键字
super关键字有两种用法:第一种是调用超类的构造函数;第二种是访问被子类隐藏的成员。
使用super调用父类构造方法时,super()方法需要在子类构造函数的第一行使用。
class B extends A{
publicc B(int a){
super(a);
System.out.println("B function")
}
}
super()会根据参数列表,匹配调用父类对应的构造函数。当多层继承时,super()总是调用该类的直接超类(自己继承的类)
super还可以用来访问被子类所隐藏的成员,这个和this关键字有些类似,只不过super只会调用超类。
class A {
int a = 1;
public void show(){
System.out.println(a);
}
}
class B extends A{
int a = 2;
public void show(){
System.out.println("child a:" + a);
System.out.println("parent a:" + super.a); //因为父类a变量被子类隐藏了,只能使用super关键字调用
}
}
构造函数的调用顺序
比如我们有一个继承关系:C继承B,B继承A。那么当我们创建对象C、B、A的构造方法都是在什么时候被调用呢?
public class A{
public A(){
System.out.println("function A");
}
}
public class B extends A{
public B(){
System.out.println("function B");
}
}
public class C extends B{
public C(){
System.out.println("function C");
}
}
C c = new C();
//打印内容:
function A
function B
function C
可以看到构造函数调用顺序是从超类到子类的继承顺序调用构造函数的。而且无论是否使用super(),因为super()总是在子构造函数的第一行调用(这就是为啥要第一行调用)。
之所以继承顺序调用,是因为父类本身不知道子类任何情况,并且有可能子类的构造函数的初始化还有可能依赖于父类的初始化。
方法重写与方法动态调度
子类可以重写父类的方法,那么在子类调用被重写的方法时,总会调用子类定义的版本,除非使用super关键字。
class A{
public void show(){
System.out.println("A");
}
}
class B extends A{
public void show(){
System.out.println("B");
}
}
class C extends A{
public void show(){
System.out.println("C");
}
}
class Test{
public static void main(String[] args) {
A a = new A();
B b = new B();
C c = new C();
//超类引用重写方法时,会根据所引用对象的类型决定调用哪个版本
a = b;
a.show();//打印B
a = c;
a.show();//打印C
}
}
方法重写和将子类对象赋值给父类变量,是动态方法调度的基础。动态方法调度机制是通过运行时确定调用方法,而不是编译时就去解析所调用的方法。
动态方法调度是Java运行时多态的机理。
抽象类
继承更多的是应用在继承抽象类中,抽象类定义抽象内容,但不提供抽象方法的实现。
任何包含抽象方法的类都必须是抽象类,抽象类中可以定义抽象方法或者普通方法。子类如果要实现抽象类,则必须实现所有抽象方法,否则子类也应该被声明抽象类。
public abstract class A{
abstract void show();
public void showA(){
System.out.println("A")
}
}
public class B extends A{
public void show()
}
抽象类不能使用new关键字创建对象,因为抽象类的定义本身就是不完整的。抽象类中也不能声明抽象的构造方法(可以声明具体实现的构造方法),也不能声明静态方法,但是能够声明静态变量。
继承中使用final关键字
final关键字除了定义常量,还有两种使用方式,就是在方法前使用final修饰和在类定义前使用final修饰。
class A{
final void mothd(){
}
}
final class A{
}
如果使用final修饰方法,则该方法不能被子类所重写(可以知道abstract和final不能被同时修饰在方法上)。如果修饰类,则该类不能被继承。
final修饰的方法,可以提高系统性能。因为编译器可以自由地内联这类方法,内联是final方法才会有的。一般方法需要动态分析方法的调用,这称为后期绑定。但是final方法不能被重写,所以final方法调用可以在编译时解析,这称为早期绑定。
接口
接口定义
使用interface关键字来定义一个完全抽象的接口(在JDK8接口有默认实现了),接口定义和类相似,但是接口没有实例变量(接口中的所有变量都是final static的)。接口可以被任意数量的类实现,一个类也可以实现任意数量的接口。
access interface name{
return-type method-name(parameter-list);
}
接口的访问控制和类一样,但是只有public和默认权限。对于默认权限,只有声明接口所在的包才能访问接口(类是只有声明类的包能够访问默认权限类),如果接口被声明为public,则能够被所有类访问。
接口中定义的变量都被隐式的使用public、final和static,所以实现接口的类不能修改它们(只能直接使用),并且在定义的时候就需要进行初始化。
public Config{
String path="/usr/local/config";
//等价于
public static final String path="..";
}
接口中的所有访问也被隐式的标识为public访问类型,所以所有实现类实现方法的时候,都需要使用public修饰实现方法。接口中的方法,可以省略abstract关键字,这个是默认隐式添加进去的。
接口还可以定义在某个类或某个接口中,这种接口称为成员接口或嵌套接口。嵌套接口可以使用public、private或protected修饰,如果在访问权限之外使用嵌套接口,那么必须使用外嵌类或外嵌接口名称加上内嵌接口使用。
public class A{
public interface B {
abstract String getString();
}
}
public class C implements A.B{
public String getString(){
}
}
A.B c = new C();
接口可以通过extends继承另一个接口,如果一个类实现的接口实现了其它接口,那么这个实现类需要实现这个接口继承链的所有方法。
实现接口
实现接口可以使用implement关键字,一个类可以实现多个接口。
public className [extends superclass] [implements interface[,interface]]{
//class-body
}
接口和抽象类一样都可以使用接口变量来接受实现接口类的引用,并且满足运行时多态,也就是在调用的过程中根据引用了类型来调用具体方法,而不是在编译过程中知道所调用的方法,使用方式和抽象类的运行时多态性一样。
如果一个类实现接口,但是没有全部实现接口的中抽象方法,那么这个类就是抽象类,该抽象类的派生子类必须实现抽象方法。
默认接口方法
JDK8为接口添加了一个新功能,叫做默认方法。默认方法允许接口定义默认实现。
之所以开发默认接口主要有以下原因:
- 提供了一个扩展接口的方法,而不破坏现有代码。当我们扩展一个接口时,如果没有默认方法实现,那么所有实现类都需要去实现这个新方法。
- 接口中的方法本质是可选的。对于一些实现类,可能并不需要使用接口中的特定方法,但是实现接口时还需要为这个方法实现写一个占位符。
public interface MyIF{
int getNumber();
default String getString(){
return "interface default str";
}
}
对于实现类,如果默认方法不能够够满足,实现类就需要重新实现该方法。
如果一个实现类实现了两个接口,这两个接心相同名称默认方法,并且实现类自身也实现了该方法,那么调用的时候会调用哪一个?
在所有情况中,类实现的优先级高于接口默认实现的。但是如果类没有实现该方法,这时候编译器就会报错,因为它不知道调用哪个接口的默认方法。但是如果要调用接口中的默认方法,则可以使用super关键字。
interfaceName.super.methodName();
JDK8除了能够定义默认方法,还可以在接口中定义静态方法。接口中定义的静态方法可以独立于任何对象使用。
public interface MyIF{
static int getDefaultNumber() {
return 0;
}
}
MyIF.getDefaultNumber();
接口与抽象类的区别
接口和类之间的决定性区别就是类可以维护状态信息,但是接口不可以。
包
包(package)是多个类的容器,它用于保持类的命名空间相互隔离,这样便于管理类。包是以分层方式进行存储,也就是每一层对应着一个目录。
//java源文件的第一行
package pkg;
如果要使用包中类有两种方式:一种是通过import方式将定义包中类引入到当前类中,另一种是通过包名+类名的全路径引用。
import java.util.*;
class MyDate extends Date{
}
class MyDate extends java.util.Date{
}
private、默认访问权限、protected和public只适用于类成员访问控制。对于非嵌套类本身只有两种访问级别:默认级别和共有级别。如果类声明为public,那么所有代码都能够访问,这时候这类必须是所在文件中唯一声明的共有类,并且文件名称与之相同。如果类使用默认访问级别,则只能被相同包中的其它成员访问。
包查找与CLASSPATH
当我们定义完成包之后,现在有一个问题,就是Java运行时系统如何才能知道去什么地方查找所创建的包?Java提供了三种机制查找包路径。
- 默认情况下Java运行时系统使用当前工作目录作为起始点,所以如果包位于当前目录的子目录,就能够找到它。
- 可以通过CLASSPATH环境变量指定包路径。
- 可通过java或javac的-classpath选项为类指定路径。
后两种情况,指定路径也需要指定包的上一级目录。
垃圾回收
我们知道java对象是通过new运算符进行创建的,但是可能会想如何销毁这个对象,释放内存空间。在C++中,是通过显示调用delete运算符来手动释放的,而java中不需要。java是通过自动解除分配的内存,而完成该工作的技术就是垃圾回收(garbage collection)。当一个对象的引用不存在时,就会认为该对象不再需要,可以被回收该对象所占的内容。
finalize方法
有时我们可以会依赖一些java系统外部资源,希望当对象被销毁前将这些资源释放掉,java提供一种终结器(finalization)机制。通过使用终结器机制,可以定义当对象被垃圾回收器收回前进行特定的操作。但是一般释放资源不使用该方法,因为不知道什么时候才会执行该方法,甚至在一些情况下有可能不执行该方法(对象超出其作用域时不会被执行)
//finalize()方法定义在Object类中
@Override
protected void finalize() throws Throwable {
//对象回收前会调用该方法
}
网友评论