Java快速入门指南
Java快速入门指南
本文旨在帮助学习过其他语言(尤其是面向对象语言)如C++、Rust的人快速上手Java。 如有错误还请指正
简单语法介绍
- 声明函数/变量时和C/C++一样类型前置
- 对于类名和包名需要使用大驼峰(首字母大写的驼峰)命名,变量/函数需要使用小驼峰命名
- java是一个面向对象的语言,也就是有”类”这一概念。
数据类型
java中的类型分为基本数据类型和引用数据类型,基本数据类型即boolean
布尔,byte
8位有符号整数,short
16位有符号整数,int
32位有符号整数,long
64位有符号整数,float
32位浮点数,double
64位浮点数。引用数据类型即其他的类(class
)。
基本数据类型与其他语言无大区别,就不赘述了,这里说一下引用数据类型的一些需要注意的点。
引用数据类型顾名思义,其是一个“引用”。这类似于C++中的指针。例如:
0
A a;
这是一个A类型的变量a吗?更严格的说,这其实是一个A类型的指针a。在c++中这段代码就创建了一个A类型的实例,但是在java中,你创建了一个A类型的指针,此时这个指针是一个空指针,因为你没有给他赋值。
0
1
A a=get();
A b=a;
这段代码通过get函数获得了一个A类型的对象,放在a中,令b赋值为a。在c++中,a会整个复制一份给b。而在java中a和b都是指针,这两行代码会获得两个指向相同内容的指针,只复制了指针,没有复制所指向的内容。
0
1
2
int a=1;
int b=a;
a=2;
此时b是多少?
b是1。因为int是基础类型而非引用类型,基础类型的赋值即直接复制数据,修改a与b无关0
A a=new A(123,"abc");
创建对象要使用new关键字,A为类型名,小括号中的即为构造函数的传参。java中new是主要的创建对象的方法。
包装类型
在有的时候,某些函数需要接收一个引用类型,而由于基础类型不是引用类型不能使用,怎么办呢?
java提供了一种包装类型,例如int对应的包装类型为Integer,long对应的是Long,char对应Character(都是首字母大写的全拼)。基础类型可以直接转换到对应的包装类型,包装类型也可以之间转换到其对应的基础类型。
0
1
2
3
4
5
6
7
8
public class A
{
public static void main(String[] args)
{
int x=0;
Integer y=x;
int z=y;
}
}
包装类型也可以进行+-*/之类的运算,但是请注意,由于其是包装类型有一些特性
- 包装类型是可以为null的,当从一个包装类型转换为基础类型时,注意若为null会出错
- 在基础类型中很多转换可以直接进行,例如int转为long。但包装类型本质上是指针,Integer和Long之间不能转换,因为它们相当于没有任何关系的两种类型的指针。相当于转换只有两种,一个基础类型和它对应的包装类型转换,基础类型之间转换。包装类型之间的转换、基础类型和不对应的包装类型之间的转换 都是非法的,会引起错误。
综上,包装类型主要用途有二:
- 当这个数需要可以是null的时候
- 当它由于各种原因必须是引用类型的时候
而在做运算等其他时候,请转换到基础类型使用,从而尽量避免各种错误。
数组
在java中数组的定义是这样的
0
1
2
3
4
5
6
// 类型名[] 变量名;
// 例如定义一个A类型的数组a
A[] a;
// 二维数组
A[][] b;
// 20维数组
A[][][][][][][][][][][][][][][][][][][][] c;
注意,[]里面就是什么也不写,因为数组类型其实是一种上面提到的引用类型。上面的a,b,c就是3个数组类型的指针,现在他们都是空指针。
0
1
2
3
4
5
6
7
8
9
// 创建一个长队为10的int类型数组
int[] x=new int[10];
// 10*10的int二维数组
int[][] y=new int[10][10];
x[1]=1;
x[4]=x[5];
y[1][4]=x[1];
y[9][1]=y[9][8];
x[1]=x[0];
创建数组也是使用new关键字,语法如上。使用上与其他语言无异。
0
1
2
3
int[] x=new int[999999];
int[] y=x;
x[114514]=10;
println(y[114514]);
这个程序的输出?
答案是10。因为数组类型也是引用类型,引用类型的赋值操作不会复制数据,x和y其实是同一个数组。包
java中的包与C/C++/C#中的namespace相似。在一个java项目中会有许多的源代码文件,这些文件中有很多类,这些文件和类自然不能全部堆放在一起,于是就需要有“文件夹”一样的东西将这些文件和类分类。与文件夹一样这些包也可以嵌套。多层的包用”.”(没有双引号)连接。例如”me.nullaqua”类似于一个叫做me的文件夹中有一个叫做nullaqua的文件夹。
一般一个项目的代码不应该直接放在根目录下,因为一个java项目中会有很多依赖的其他项目,如果所有项目都写在根目录下,只要这些依赖中有两个相同名字的类就过不了编译了。按照规范,一个项目的代码应该放在 “倒着写”的域名.项目名 下。例如,我的域名是nullaqua.me
,那么我的某一项目api
的所有代码应该都放在me.nullaqua.api
包下。
文件的路径要和文件中声明的包匹配,例如当你创建了一个文件,其中声明的内容在me.nullaqua
包下,那么你需要在源代码文件夹下创建一个me
的文件夹,然后在me
中创建nullaqua
,然后把这个文件放进去,即放在me/nullaqua
文件夹下。
在一个源代码文件中声明所属的包需要在第一行声明,例如这个我从我的项目里复制的文件
0
1
2
3
4
5
6
7
8
9
10
11
12
13
package me.nullaqua.api.util;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class StringsUtils
{
public static List<String> toStrings(Object o)
{
// TODO
}
}
其第一行声明了这个文件是属于me.nullaqua.api.util
包的
类的基础知识
java是一个严格面向对象的语言,在java中万物皆对象。因为这一特点所以“类(Class)”在java中十分重要。java中的类与C++中的 结构体(struct)/类(Class)、Rust中的struct和impl本质一样。
方法Method
java中的方法(Method)对应其他语言中的函数/成员函数。但与c/c++/rust不同的是java中的所有方法必须在类中定义。
字段Field
java中的字段(Field)对应其他语言中的成员变量。java中没有类似C++中的全局变量,变量要么在函数中定义,其作用域在函数内,要么作为成员变量在类中定义。
不可变变量
在变量类型前加final以声明不可变的变量/字段
0
1
2
3
int x=0;
x++; // ok
final int y=0;
// y++; error!
对于基础类型不可变就是不可变,但是对于引用类型,这只是指针不变,也就是总是对应一个对象,但对象中的内容可能会变化
也就是说,如下Java代码:
0
final A a;
其实接近C++中的:
0
A* const a;
而不是:
0
const A* a;
小练习:如下2个代码分别能否通过编译?
0
1
final int[] x=new int[10];
x[0]=1;
0
1
final int[] x=new int[10];
x=new int[10];
答案
第一个可以,第二个不可以。因为int[]是引用类型,final修饰的是指针本身,而不是指向的内容。静态方法、字段static
从上面的Method和Field可以看出,java无论是变量还是函数都必须作为某个类的成员函数/成员变量。而这意味着需要创建对应的类的实例才能使用。难道java中没有类似于C中的全局变量、函数吗?当然是有的。
0
1
2
3
4
5
6
7
8
9
10
class A
{
void method()
{
// TODO
}
static void staticMethod()
{
// TODO
}
}
在上面的代码中创建了一个类A,其中包含一个普通的成员函数method,和一个静态函数staticMethod。
0
1
2
3
4
5
6
7
8
A a=new A();// 创建A类的实例
// 调用method
a.method();
// A.method(); // 报错
// 调用staticMethod
A.staticMethod();
a.staticMethod(); // Warning
从这个示例可以看出,非静态的方法需要先创建实例,在通过实例调用。而静态实例可以直接使用类名调用,调用不同的实例静态函数是没有意义的,静态函数不与实例绑定,正在上面的例子中可以理解为每创建一个A的实例,就会“多”一个method。每个method都需要绑定在一个实例上,而静态方法不会随着实例的创建而“创建”,可以理解为静态方法是与A类绑定的。虽然可以使用实例调用静态方法,但是本质上调用的都是一个,所以为了避免歧义,应该统一使用类名来调用静态函数。使用实例调用静态函数会报Warning
了解静态方法之后静态字段也是同样的逻辑
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
int cnt=0;
static int staticCnt=0;
void plus() { cnt++; }
static void staticPlus()
{
// cnt++; plus(); 报错
/*
注意,从上面的代码中可用看出,
静态方法其实不属于某个实例,
既然没有实例就不能调用非静态的字段/方法
*/
staticCnt++;
}
}
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
A a=new A();// 创建A类的实例
A b=new A();
// 此时a
// 调用
a.plus(); // a中cnt 0->1
b.plus(); // b中cnt 0->1
b.plus(); // b中cnt 1->2
A.staticPlus(); // A中staticCnt 0->1
a.staticPlus(); // A中staticCnt 1->2
b.staticPlus(); // A中staticCnt 2->3
println(a.cnt); // 1
println(b.cnt); // 2
println(A.staticCnt); // 3
访问权限
在类中,有时需要定义一些仅在内部使用,而不希望被外部调用的方法/字段。
在java中,访问权限分为4个等级
类内部可以访问 | 同一包中可访问 | 子类可访问 | 任何类可访问 | |
---|---|---|---|---|
private | true | |||
package-private | true | true | ||
protected | true | true | true | |
public | true | true | true | true |
*子类参见类的继承
声明方式: 除package-private
为默认无需特别声明外,其他等级直接写在方法/字段类型之前
0
1
2
3
4
5
6
7
8
9
public class PublicClass
{
private int privateInt=0;
int packagePrivateInt=0;
protected int protectedInt=0;
public int publicInt=0;
private void privateMethod(){}
void packagePrivateMethod(){}
// 后面懒得写了
}
- 类也有访问权限之分,这里先不讨论inner类,类只有两种访问权限,public即任何类可以访问,和什么都不写,也就是package-private,仅当前包内可以访问。java有一个要求,每个java源代码文件中只能有一个public的类,且这个类的名字需要与文件名相同。比如上面的代码必须写在一个PublicClass.java文件中。
- 通过上面几个部分可以看出一个方法/字段前要写4个东西,访问权限、静态与否、是否可变、类型。其中类型是必须的,而且必须写在最后,即方法名/字段名之前。而访问权限、静态与否、是否可变这3个顺序是不要求的,怎么写都能通过编译,效果一样,但是一般按照访问权限、静态与否、是否可变的顺序写。
0
1
2
3
4
5
6
7
8
9
class A
{
public static final int a=0;//可以,推荐
public final static int b=0;//可以
static public final int c=0;//可以
static final public int d=0;//可以
final public static int e=0;//可以
final static public int f=0;//可以
// public static int final a=0;//不可以!!!
}
Hello world
来一个经典的hello world,java版
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package subit.example; // 所属的包
// 主类,其中需要包含主函数,是整个程序的起点,主类的名字没有要求,但是要求其为public
public class Main
{
// 主函数,其必须是public static的,返回值必须是void,名字必须是main,传参一个字符串类型的数组
// 这一行没有什么能改的,唯一能改的就是给传参args改个名
public static void main(String[] args)
{
// System是一个类,其中有一个public static final的字段out,对应标准输出流
// 调用这个标准输出流的println函数,打印hello world
// println即为print line,后面会自动换行,无需再换行
System.out.println("hello world!");
}
}
运算
数学运算
数学运算这里不再多追述,与大多数语言相同,包含+
、-
、*
、/
、++
、--
、+=
、-=
、*=
、/=
位运算
java中也是包含位运算的,包含
~
按位取反^
(^=
) 按位异或&
(&=
) 按位与|
(|=
) 按位或<<
(<<=
) 左移>>
(>>=
) 有符号右移>>>
(>>>=
) 无符号右移
其他运算不赘述,这里主要说一下两种右移
有符号右移
x右移n位应该等价为 $x2^{-n}$ 向下取整。例如-1向右右移1位应该是 $-10.5=-0.5$ 在向下取整等于 $-1$。那么以byte类型为例,在byte类型中,$11111111$即为$-1$。注意到若右移$1$位且高位补零的化会变为$01111111$即$255$。这不符合上面的推断。所以在有符号整形中,若当前为负数,即最高位为$1$,右移后高位补$1$,否则补$0$。
无符号右移
无符号数与有符号数不同,其没有符号位,永远是非负数,这样在右移时高位永远补0。
而看上面的类型表就知道,java中没有无符号整数,而有时位移并不是只是为了进行传统的乘除法而是除了其他问题,此时需要无符号数这样位移且高位始终补0的运算。所以有了无符号右移,即将当前数作为无符号数进行右移从而让其高位始终补0。
0
1
2
3
4
5
6
7
public static void main(String[] args)
{
int r=-1;
int x=(r>>1);
int y=(r>>>1);
System.out.println(x);
System.out.println(y);
}
这段代码输出如下:
0
1
-1
2147483647
注意事项
java中隐式转换比较严格当从小类型转换为大类型时,例如int转为long,可以隐式转换而小类型转为大类型不可以。当从小类型转换为大类型时会保证保存的数一致,例如byte类型的$11111111$($-1$)转为
short
后会是$1111111111111111$($-1$)前面补1保证数值一致。而从大到小时则是直接截断。例如short
类型的$0000000111111101$(7个0,7个1,1个0,1个1,即1021)转为byte后会变为$11111101$,前面7个0和1个1直接被舍弃,而后8位的7个1和1个0原封不动的保留,而这对应byte
中的$-3$。
0
1
2
3
4
int x=0;
long y=x; //OK
long x1=0;
int y1=x1; //ERROR
小于int的其他整形,在参与运算时都会转成int进行运算
0
1
2
3
4
5
byte a=1;
byte b=2;
byte c=a+b; //错误a+b是int类型
byte c=a*b; //错误
byte c=a&b; //错误
byte c=a<<1;//错误
以上的两个注意事项如果没有注意到会引起奇怪的错误,例如:
0
1
2
3
4
byte a=-1;
byte b=a>>1;
byte c=a>>>1;
System.out.println(b);
System.out.println(c);
根据上面的无符号右移和有符号右移这里应该分别输出$-1$和$255$吧?
事实上这个代码编译不通过,因为a>>1
和a>>>1
是int
类型的,不能隐式转换到byte
。
0
1
2
3
4
byte a=-1;
byte b=(byte) (a>>1);
byte c=(byte) (a>>>1);
System.out.println(b);
System.out.println(c);
这样强制转换可以了吧?应该输出-1和255对吧?
一运行发现输出$-1$和$-1$。
小于int的类型会在“参与运算时转为int类型”而非结果转为int类型。所以a对应的8个1在运算开始前就转为了int类型,而根据上面的转换规则,其会转换为int类型里的-1即32个1。无符号右移1位,得到最高位0和后面31个1。转为byte时直接截断,得到那31个1末尾的8个1,即byte中的-1。
这就是为什么java中很多应该是byte/short为返回值/传参/变量的地方都用了int,这能避免出现这类问题。
逻辑运算
逻辑运算与大部分其他语言类似,有:
<
小于<=
小于等于>
大于>=
大于等于==
等于!=
不等于!
非&&
逻辑与||
逻辑或
其中需要注意是==
和!=
。在int,long,boolean等基础类型中“==
”和其他语言一样比较的是值是否相同。而在进行包装类型的比较时则是对比“指针是否相同”。
例如:
0
1
2
3
4
5
6
7
8
public class A
{
public static void main(String[] args)
{
A a=new A();
A b=new A();
System.out.println(a==b);
}
}
这段代码生成了两个A类实例,虽然两个A的内容没什么区别,但由于是两个不同的对象,这段代码输出false。同理!=则输出true
尤其需要注意的是包装类型之间的==
和!=
0
1
2
3
4
5
6
7
8
public class A
{
public static void main(String[] args)
{
Integer a=1;
Integer b=1;
System.out.println(a==b);
}
}
这段代码输出什么?答案是true
,1==1
很合理对吧?
0
1
2
3
4
5
6
7
8
public class A
{
public static void main(String[] args)
{
Integer a=128;
Integer b=128;
System.out.println(a==b);
}
}
这个呢?和上面没什么区别吧?也是true
?其实不好说,如果使用默认配置的话这个代码输出false
。
为什么?
java希望每个值对应一个唯一的包装类型的对象。也就是有一个Integer
对象代表int
类型的$0$的包装,一个对应$1$的包装,一个$2$……当你需要哪个时就直接拿哪个,例如需要$0$时就直接拿出代表$0$的对象使用。但是int
范围是$2^{32}$,创建这么多对象显然不可能。
那如果每次需要什么就创建一个呢?例如需要1就创建一个新的代表1的Integer的对象。但是这会导致创建的两个1不是一个对象。
最后java选择了一个折中的(离谱的)方案,以int为例java创建了-128~127共计256个对象,第一个代码中a和b相等因为0在-128~127之间,就之间使用了早已创建好的对象。所以他们是一个对象,相等很合理。而在第二个代码中,128不在范围内,所以需要代表128的Integer对象时会每次创建一个新对象。这意味着这里的a和b是不同对象,且包装类型是引用类型,==
在判断引用类型是否相等时判断的是它们是不是一个对象而非内容是否相同,而a
和b
正是不同对象,所以输出了false
。
那为什么前面说“不好说”呢?因为-128~127这个范围并非固定,在运行代码时通过运行参数可以修改这个保存的范围。
而若是对比Integer和Long是否相同则总是为false,因为他们甚至是不同的类,即使保存的值其实相同,他们也总是不相等,而基础类型的int和long只要值相等,==
即返回true;
所以:
不要在包装类型中使用
==
和!=
不要在包装类型中使用
==
和!=
不要在包装类型中使用
==
和!=
枚举类
有如下一个场景:
0
1
2
3
4
5
6
class Dog
{
public <AType> getState()
{
// TODO
}
}
我们有一只狗,有一个方法获得其当前状态,状态有3种:活跃,睡觉,吃饭。这个函数的返回值应该是什么类型呢?
可以是int,并约定0代表睡觉,1代表活跃,2代表吃饭,但这样并不方便,还要判断出现114514这样不符合约束的值的出现。
所以就有了枚举类。
0
1
2
3
enum State
{
SLEEP, EAT, PLAY;
}
这就创建了一个枚举类。
枚举类也是类,但是其最大的区别是这个类的对象是有限,且已知的。例如在上面的例子中,State这个枚举类只有3个对象,分别是SLEEP,EAT,PLAY。你无法通过其他方式创建对象,也就是说new State();绝对过不了编译。
最上面的代码就可以改为这样:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dog
{
public State getState()
{
if (/* TODO */true)
{
return State.SLEEP;
}
else if (/* TODO */true)
{
return State.EAT;
}
else
{
return State.PLAY;
}
}
}
可以看出,取用这些对象时就像取出State类中的静态字段一样。上面的逻辑运算章节说了==
在判断引用类型时的潜在隐患,但是对于枚举类型基本没有这类问题,因为其对象是确定的,例如上面的State中,EAT永远等于EAT,永远不等于PLAY。
枚举类除了声明时用enum关键字、对象必须罗列在最前面,其他方面和普通类没什么区别。
例如这样
0
1
2
3
4
5
6
7
8
9
enum State
{
SLEEP, EAT, PLAY;
int x=0;
void run()
{
// TODO
}
}
在对象中创建了一个字段x和方法run。其实这段代码和以下代码功能基本相同
0
1
2
3
4
5
6
7
8
9
10
class State
{
public static final State SLEEP=new State();
public static final State EAT=new State();
public static final State PLAY=new State();
int x=0;
void run()
{
// TODO
}
}
不同的是后者可以通过new关键字创建新的对象,而前者不可以。这样就好理解了吧。
枚举类自然也是可以有构造函数的,
0
1
2
3
4
5
6
7
8
9
10
enum State
{
SLEEP(0), EAT(1), PLAY(2);
int x;
State(int t)
{
x=t;
}
}
这约等于
0
1
2
3
4
5
6
7
8
9
10
class State
{
public static final State SLEEP=new State(0);
public static final State EAT=new State(1);
public static final State PLAY=new State(2);
int x=0;
State(int t)
{
x=t;
}
}
区别还是不能new新的对象。
继承
基本
举个例子,我们现在有一个Cat类,还有一个Dog类。
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Cat
{
void shout()
{
System.out.println("喵喵喵");
}
void sleep()
{
System.out.println("zzzzz");
}
}
class Dog
{
void shout()
{
System.out.println("汪汪汪");
}
void sleep()
{
System.out.println("zzzzz");
}
}
注意到它们有完全一样的sleep函数,这部分代码是重复的,而且假设我们还有一个方法makeAnimalSleep,接收一个Cat或Dog,调用它的sleep函数,这个函数应该接收什么类型的传参呢?于是就需要类的继承了
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal
{
void sleep()
{
System.out.println("zzzzz");
}
}
class Cat extends Animal
{
void shout()
{
System.out.println("喵喵喵");
}
}
class Dog extends Animal
{
void shout()
{
System.out.println("汪汪汪");
}
}
在这里,我们创建了一个类Animal,然后在这个类中定义了sleep方法。之后让Cat和Dog“继承”了Animal。
我们称继承别的类的类(这里的Cat和Dog)为“子类”。而被继承的类为“父类”或“超类”(这里的Animal)。(父类在英文中为super class故有时直译为超类)这种称呼与数学中集合的超集子集类似。
继承中的子类可以被简单理解是父类的一种“特殊化”。也就是说上面例子中的Cat和Dog是一种特殊的Animal。而Animal是Cat和Dog的公共部分。
0
1
2
Cat cat = new Cat();
cat.sleep();
cat.shout();
对于子类的对象,可以直接调用父类的方法。
既然子类被认为是特殊的父类,也就是Cat是特殊的Animal那么一只猫就是一个动物。一个Cat的对象也是Animal的对象。也就是说子类的对象可以直接作为父类的对象使用。
0
Animal animal = new Cat(); // OK
这样通过继承我们便可以实现上面提到的makeAnimalSleep
0
1
2
3
4
5
6
static void makeAnimalSleep(Animal animal)
{
animal.sleep();
}
makeAnimalSleep(new Cat());
makeAnimalSleep(new Dog());
注意,在java中每个类只能有一个父类,这与c++等语言的多继承不同。
重写
上面我们解决了需要写两个一样的sleep方法的问题。现在我们想实现一个makeAnimalShout的函数。该如何实现呢?对于一个Animal的实例,虽然Cat和Dog中都有shout,但是我们肯定不能在Animal对象上直接调用shout方法,因为Animal类中没有shout方法。所以我们可以在Animal中定义一个shout方法,再让Cat和Dog“覆盖”这一方法。这就是重写。
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Animal
{
public void sleep()
{
System.out.println("zzzzz");
}
public void shout()
{
System.out.println("不知道怎么叫");
}
}
class Cat extends Animal
{
@Override
public void shout()
{
System.out.println("喵喵喵");
}
}
class Dog extends Animal
{
@Override
public void shout()
{
System.out.println("汪汪汪");
}
}
这里的@Override不是必须的,不写也可以,但是一般情况下最好写上来标注该方法是重写的。对于函数名和函数参数类型与父类相同的方法,会自动认为是重写父类的方法。
0
1
2
3
4
5
6
7
8
9
public static void main(String[] args)
{
makeAnimalShout(new Cat());
makeAnimalShout(new Dog());
}
static void makeAnimalShout(Animal animal)
{
animal.shout();
}
最终输出:
喵喵喵
汪汪汪
可见即使将子类的对象(Cat/Dog)转为了父类(Animal)其本质上依然是子类,调用shout方法的时候,依旧调用的是子类的方法。
那么如果我就想调用父类的方法呢?
被重写覆盖掉的方法,只能在覆盖它的子类中调用
0
1
2
3
4
5
6
7
8
class Cat extends Animal
{
@Override
public void shout()// 调用该函数后输出
{
super.shout();
System.out.println("喵喵喵");
}
}
改方法调用后输出:
不知道怎么叫
喵喵喵
有一个关键字super和this类似,this代表自己,而super也代表自己,不过是父类的自己。通过super.shout就可以调用父类的shout。不过由于super无法被外界调用(比如说这里的super只能在Cat类内调用表示Cat的父类)所以被重写的方法只有重写这个方法的子类能调用。
抽象
上面的例子中还有两个不够完美的地方,第一Animal本身是一个普通的类,依然可以通过new Animal创建一个Animal实例。这是我们不希望的。第二为了能对Animal对象直接调用shout方法,我们在Animal中声明了shout方法。但是事实上,这个方法不应该被调用,他唯一的作用应该是表示“子类中会有这个方法”。这就需要用到抽象了。
叫本身就是一个抽象的行为,每种动物叫都不一样,人们从中抽象出了“叫”这种行为。动物本身也是一种抽象的东西。人们从猫狗中抽象,统称它们为动物。在java中抽象也差不多。
0
1
2
3
4
5
6
7
abstract class Animal
{
public void sleep()
{
System.out.println("zzzzz");
}
abstract public void shout();
}
这里我们通过abstract关键字将Animal和shout都声明为了抽象的。抽象的类无法实例化,也就是这里不能直接new Animal。但抽象的类可以被继承。并且,子类必须重写父类的抽象方法。也就是这里的shout。抽象的方法没有任何实现,这意味着该方法不能被调用,它只是标志着继承该类的方法必须需要实现这样的一个shout方法。
“存在抽象方法”是“该类为抽象类”的充分不必要条件。即类是抽象的可以没有抽象方法。但是有抽象方法该类一定是抽象的。
接口
考虑这样一个问题,能移动的不一定是动物,动物也不一定能移动。所以我们定义了这样两个类
0
1
2
3
4
5
6
7
8
9
10
abstract class Animal
{
abstract public void sleep();
abstract public void shout();
abstract public void eat();
}
abstract class Moveable
{
abstract public void move();
}
由于java中只能有唯一的父类,所以如果我们定义一个Dog类,我们希望他同时继承Animal和Moveable,但这在java中是不行的。于是就有了接口。
接口是一种特殊的类,他可以被认为是一个抽象类,与一般的抽象类不同的是:
- 接口中的所有方法都必须是抽象的(静态方法除外)
- 接口中不能定义字段(静态字段除外)
- 接口中的函数都必须是publlic的 (静态方法除外)
- 接口可以继承其他接口,但是不能继承其他的类或抽象类
- 一个类只能有一个父类,但是可以继承多个接口,类或抽象类继承接口被称之为“实现”接口。
有了接口,我们就可以这样实现上面的功能。
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
interface Animal
{
void sleep(); // 因为接口中的方法都必须是public且abstract,所有写不写public和abstract都可以没有区别
void shout();
void eat();
}
interface Moveable
{
void move();
}
class Dog implements Animal, Moveable // 通过关键字implements来实现接口
{
@Override
public void sleep()
{
System.out.println("Dog is sleeping");
}
@Override
public void shout()
{
System.out.println("Dog is shouting");
}
@Override
public void eat()
{
System.out.println("Dog is eating");
}
@Override
public void move()
{
System.out.println("Dog is moving");
}
}
其他
- final关键字:在变量上表示不可变。在方法上代表类被继承后该方法不可以被重写。
- protected关键字: 上面的访问权限中有提到,同包内和子类可以访问。
- 重写只能重写子类能访问的方法,例如如果某一方法是private的子类不能访问,就不能重写。具体情况具体分析,例如子类和父类在统一包呢,就能重写包内可访问的方法(default访问权限),否则就不行。
异常
先举一个例子:
0
1
2
3
4
5
6
7
public class A
{
public static boolean fileIsExist(String path)
{
// TODO
return true;
}
}
我们有一个方法,用于检查文件是否存在,如果文件存在返回true,不存在返回false。但如果因为当前程序没有权限访问文件,或者因为系统错误无法访问文件应该返回什么呢?
可以使用上面的枚举类解决
0
1
2
3
4
5
6
7
8
9
10
public class A
{
public static State fileIsExist(String path)
{
}
}
enum State
{
EXIST,NOT_EXIST,ERROR
}
但如果错误种类更多呢?或者我想知道更多错误信息。这会导致返回值乱七八糟,明明只是存在与否还是应该使用boolean更合理,而对于这类错误,就需要一种特殊的退出函数的方式,代表函数出错,且没有返回值,这就是异常。
0
1
2
3
4
5
6
7
8
9
10
11
12
public class A
{
public static void main(String[] args)
{
boolean b = fileIsExist("/a/path");
}
public static boolean fileIsExist(String path)
{
// TODO
// 如果出现异常
throw new RuntimeException("出现未知错误");
}
}
如上,可以使用throw抛出一个错误,错误也是一个对象,一般在需要抛出错误时new一个新的对象,这与调用栈有关。
调用栈
运行上面的代码可以看到输出:
Exception in thread "main" java.lang.RuntimeException: 出现未知错误
at A.fileIsExist(A.java:11)
at A.main(A.java:5)
这个输出包含了3个信息
- 出错的线程
Exception in thread "main"
即main
线程出现错误 - 错误信息
java.lang.RuntimeException: 出现未知错误
错误的类是RuntimeException
, 信息是出现未知错误
- 调用栈, 可以看到整个程序方法调用情况, 方便排查错误
A.main
方法(A.java
文件的第5行) 调用A
.fileIsExist方法(A.java
文件的第11行) <– 在最后这里抛出错误
上面提到”一般在需要抛出错误时new一个新的对象”这是因为错误的调用栈是在new时创建的, 所有如果不在需要抛出错误时new, 会导致调用栈不正确
异常捕获
在上面的例子中, 如果出现异常最终会导致整个程序停止运行, 但有我们不希望异常终止程序, 而是对异常进行处理. 比如在上面的例子中, 当出现错误时不应该终止程序, 可能我们希望向用户弹出弹窗提醒用户. 这时就需要捕获异常
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class A
{
public static void main(String[] args)
{
try
{
boolean b=fileIsExist("/a/path");
}
catch (RuntimeException e)
{
e.printStackTrace(); // 打印错误的调用栈
// TODO 弹出对话框
}
}
public static boolean fileIsExist(String path)
{
// TODO
// 如果出现异常
throw new RuntimeException("文件不存在");
}
}
通过try和catch关键字, try后面写可能出现错误的代码, catch后面的小括号和函数传参类似, 先是接收的错误的类型, 后面的e则是名称, 之后就可以针对错误进行处理.
这里的RuntimeException即catch的异常类型, 若抛出的错误是这个类型的子类, 会进入到catch中, 若不是则不会.
一个try可以有多个catch
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class E0 extends RuntimeException {}
class E1 extends RuntimeException {}
public class A
{
public static void main(String[] args)
{
try
{
boolean b=fileIsExist("/a/path");
}
catch (E0 e)
{
// TODO
}
catch (E1 e)
{
// TODO
}
}
public static boolean fileIsExist(String path)
{
// TODO
// 如果出现异常
throw new RuntimeException("文件不存在");
}
}
这允许对于不同类型的错误有不同的处理
同时也可以对于不同错误相同处理通过|
符号连接多个错误类型, 一同处理
0
1
2
3
4
5
6
7
try
{
boolean b=fileIsExist("/a/path");
}
catch (E0|E1 e)
{
// TODO
}
顺序问题:
看这样一个例子:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class E0 extends RuntimeException {}
class E1 extends E0 {}
public class A
{
public static void main(String[] args)
{
try
{
boolean b=fileIsExist("/a/path");
}
catch (E0 e) // 1
{
// TODO
}
catch (E1 e) // 2
{
// TODO
}
}
public static boolean fileIsExist(String path) { throw new E1(); }
}
此时编译会报错, 原因是E1是E0的子类, 所以在1处就会被处理掉, 2永远不可能进入.
而如果调换E1和E0
0
1
2
3
4
5
6
7
8
9
10
11
try
{
boolean b=fileIsExist("/a/path");
}
catch (E1 e) // 1
{
// TODO
}
catch (E0 e) // 2
{
// TODO
}
则对于E1类型的错误只会进入1而不会进入2
受检异常
异常分为两大类, 受检异常和非受检异常, 首先所有异常都是Throwable的子类, 而Throwable有一个特殊的子类就是上面的RuntimeException. 对于是Throwable的子类, 而非RuntimeException的子类的错误. 被称之为受检异常.对于受检异常有两点不同:
- 必须的throws
0
1
2
3
public static boolean fileIsExist(String path) throws Throwable
{
throw new Throwable();
}
在方法后使用throws指出可能抛出哪些异常, 对于非受检异常, 即RuntimeException的子类, 可以在throws后面写出, 用以提醒其他开发者等功能, 而受检异常必须在throws后面写出, 否则不过编译
- 必须的try
0
1
2
3
4
5
6
7
8
9
10
11
public class A
{
public static void main(String[] args)
{
boolean b=fileIsExist("/a/path");
}
public static boolean fileIsExist(String path) throws Throwable
{
throw new Throwable();
}
}
在最开始的使用RuntimeException的例子中可以通过编译, 但在这个例子中, 无法通过编译. 原因是fileIsExist可能抛出非受检的异常Throwable, 而对于非受检异常不能忽视. 即不处理直接继续向上抛出, 除里方法一般有3种:
- try: 上面已经说过try的用法了, 第一种方法即使用try处理掉异常
- 声明throws: 如果就是希望错误继续向上抛出, 可以在main方法上加上 throws Throwable 这样可以允许错误继续向上抛出而不处理
- 包装: 这种情况算是第一种处理方法的特例
0
1
2
3
4
5
6
7
try
{
boolean b=fileIsExist("/a/path");
}
catch (Throwable e)
{
throw new RuntimeException(e);
}
RuntimeException等很多异常都有一个传参为其他异常的构造函数, try捕获非受检异常后可以通过这个方法将其转换为非受检的异常继续抛出
方法继承的异常问题
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class A
{
public void x()
{
// TODO
}
}
class B extends A
{
@Override
public void x() throws Throwable
{
// TODO
}
}
这个代码无法通过编译, 原因是B中的x重写了A中的x, 而B中的x需要抛出非受检的错误, A却不需要. 众所周知子类可以转换为父类, 当B转换为A的时候, A没有声明抛出非受检异常, 即不可能抛出非受检异常. 而B却需要抛出, 引起矛盾. 所以当出现方法重写时, 不能抛出父类中不可能抛出的异常. 例:
这些是不允许的:
0
1
2
3
4
5
6
7
8
public class A
{
public void x() {}
}
class B extends A
{
@Override
public void x() throws Throwable {}
}
0
1
2
3
4
5
6
7
8
9
class E extends Throwable {}
public class A
{
public void x() throws E {}
}
class B extends A
{
@Override
public void x() throws Throwable {} // B中可能抛出的异常不是A可能抛出的异常的子集
}
这些是允许的:
0
1
2
3
4
5
6
7
8
public class A
{
public void x() throws Throwable {}
}
class B extends A
{
@Override
public void x() {}
}
0
1
2
3
4
5
6
7
8
public class A
{
public void x() throws Throwable {}
}
class B extends A
{
@Override
public void x() throws Throwable {}
}
0
1
2
3
4
5
6
7
8
9
class E extends Throwable {}
public class A
{
public void x() throws Throwable {}
}
class B extends A
{
@Override
public void x() throws E {}
}
特别的是RuntimeException, 即使A中没有声明抛出RuntimeException, B中也可以抛出RuntimeException, 因为毕竟RuntimeException本身就不需要声明也可以抛出
0
1
2
3
4
5
6
7
8
public class A
{
public void x() {}
}
class B extends A
{
@Override
public void x() throws RuntimeException {}
}