我们不是学过了吗?
看到标题,很多人可能会想:我们之前不是讲过如何定义和使用类了吗?
是的,我们讲过了,现在我们来回顾一下。
定义类:
class 类名{
//使用变量和对象表示属性
数据类型 变量名/对象名;
//使用方法来表示行为
方法返回值类型 方法名(参数列表){
方法体;
}
}
生成和使用对象的方法是:
类名 对象名=new 类名();
对象名.属性名……
对象名.方法名……
今后,我们还会学习更多的知识。为了方便说明,今后,我们把定义在方法中的变量叫做局部变量;定义在类中的变量,也就是属性,称为成员变量;定义在类中的方法,也就是行为,称为成员方法。
封装
我们之前设定对象属性时,会使用这种形式:对象名.属性名=属性值;
。但这种形式有个缺陷:我们可以为某个属性设置符合其数据类型表达范围的任意值。换言之,我们可能将某个属性的值设置为语法正确但含义错误。
例如,某个类中包含年龄属性,无论使用整数类型还是实数类型,我们都可以把年龄设置为负数!在语法上,这是完全允许的,但现实中,没有人的年龄是负数。
因此,为了避免出现这种问题,我们可以不允许程序员直接访问这些属性,同时设计一些方法,以便程序员调用并设置属性值和获取属性值。我们可以在这些方法中做一些设置,从而减少出错的可能性。此外,我们也可以在设置属性和获取属性值的方法设置其他功能,例如MAC地址、IP地址格式的转换。
这个处理思路就叫做封装。
封装的实施步骤有两个:
- 将属性隐藏在类内部,使用
private
修饰属性即可; - 为被封装的属性设置对应的设置值和获取值的方法
- setter和getter方法一般为public,以便外界访问
在习惯上,设置属性值的方法会用set+属性名命名,获取属性值的方法会用get+属性名命名,因此有时会称之为setter和getter方法。
看下面的例子:
public class Student {
//年龄属性
private byte age;
public byte getAge() {
return age;
}
public void setAge(byte age) {
this.age = age;
}
}
这就是封装后的基本形式,age属性被设置为私有的(private),外界只能通过getAge()和setAge()方法来访问(这两个方法是public的)。不过我们会发现,这样封装和没有封装并没有任何区别,我们仍然可以把年龄设置为负值。下面我们可以改进一下setAge()方法:
public void setAge(byte age) {
if (age >= 0 && age <= 100) {
this.age = age;
} else {
System.out.println("年龄属性设置有误,该属性将会被设置为 0");
}
}
在这个改进版的方法中,我们对年龄做了一个限制,只有0到100才是合法的年龄值;如果不满足这个条件,就不会为age属性赋值——换言之,该属性将会被赋予默认值,也就是0(可以回顾一下第二部分第四小节关于默认值的内容)。
当然,数值不合法时,我们也可以将age属性赋值为-1
这样的非法特定值,这样在检查数据时可以很容易发现问题。
不过,这个版本还不够好,如果程序运行时输入的数据有问题,那么年龄就会被设置成某个默认值,这样就是用一个错误去修正另一个错误。我们可以强制使用者必须提供一个合法的数值:
public void setAge() {
Scanner sc=new Scanner(System.in);
byte age=-1;
while(true) {
System.out.println("请输入年龄:");
age=sc.nextByte();
if (age >= 0 && age <= 100) {
this.age = age;
sc.close();
break;
} else {
System.out.println("年龄属性设置有误,请重新输入");
}
}
}
在这个版本中,我们可以要求用户不停输入数值,直到输入合法为止。
在这里,我们看到了一个新的关键字:
this
。我们可以把this理解为“当前对象”,this.age就是当前对象的age,这样就可以把属性age和方法中定义的变量age区分开了。
这就是封装,我们只需要将要封装的属性用private
修饰,然后编写setXXX方法和getXXX用于设置和获取属性值即可。这两个方法使用public
修饰的。当然,也可以只编写需要的方法。
我们也可以利用Eclipse为我们提供的便利功能来快速完成这一过程,并生成方法框架。我们可以在代码编辑区上点击鼠标右键,在弹出菜单中选择Source–>Generate Getters and Setters,在弹出对话框中选择要封装的属性,展开属性项,还能看到要生成的方法,然后选择插入位置、修饰符等选项,单击Generate按钮即可。
不过,IDE帮我们生成的代码和我们写的第一种形式是相同的,还需要我们根据需要调整代码。
04
重载
有时候,我们需要在同一个作用域中(同一个类中)定义多个功能相同的方法,例如计算两个数据相加的方法,如果两个数据为int类型,我们需要写一个方法;这个方法肯定不适合计算两个double类型数据相加,所以我们需要再编写一个方法;这两个方法又不适用于两个long类型数据相加,于是我们需要编写第三个方法……习惯上,我们会根据方法的功能来给方法起名字。但根据之前所学的内容,同一个作用域中是不能出现同名的变量的,那么,多个功能相同的方法,名字能相同吗?
在有些编程语言中,是不允许的。但在Java中,这是允许的!在上面的例子中,我们可以创建三个add
方法(当然也可以起其他的名字,见名知意即可),分别计算int数据、double数据和long数据的求和问题。是不是很方便?
想必这时候,各位是高兴不起来的,因为大家应该都想到了一个问题:如果名字相同,计算机怎么知道我们想调用哪个方法呢?
答案是:依靠参数。计算机可以通过参数的三个特性来确定我们想要调用的方法:
- 参数的数据类型
- 参数的个数
- 以数据类型为标准的参数顺序
换言之,只要同名方法的参数在这三个特性中至少有一个特性是独特的,那么这个方法就可以被计算机准确识别,它就是合法的重载方法。
来看一个例子。该示例中包含两个程序,第一个是Overload.java
:
public class Overload {
// 计算两个int数据相加的方法
public int add(int x, int y) {
System.out.println("本方法的参数为两个int数据");
return x + y;
}
// 计算两个double数据相加的方法
public double add(double x, double y) {
System.out.println("本方法的参数为两个double数据");
return x + y;
}
// 计算两个long数据相加的方法
public long add(long x, long y) {
System.out.println("本方法的参数为两个long数据");
return x + y;
}
// 计算三个int数据相加的方法
public int add(int x, int y, int z) {
System.out.println("本方法的参数为三个int数据");
return x + y + z;
}
// 计算一个int数据和一个double数据相加的方法
public double add(int x, double y) {
System.out.println("本方法的参数为一个int数据和一个double数据");
return x + y;
}
// 计算一个double数据和一个int数据相加的方法
public double add(double x, int y) {
System.out.println("本方法的参数为一个double数据和一个int数据");
return x + y;
}
}
第二个是测试程序OverlaodMain.java
public class OverloadMain {
public static void main(String[] args) {
Overload o=new Overload();
System.out.println("----------两个int数据相加-----------");
System.out.println("100+200="+o.add(100, 200));
System.out.println("----------两个double数据相加-----------");
System.out.println("3.14+7.98="+o.add(3.14,7.98));
System.out.println("----------两个long数据相加-----------");
System.out.println("300L+400L="+o.add(300L, 400L));
System.out.println("----------三个int数据相加-----------");
System.out.println("100+200+300="+o.add(100, 200,300));
System.out.println("----------一个int数据和一个double数据相加-----------");
System.out.println("100+3.14="+o.add(100, 3.14));
System.out.println("----------一个double数据和一个int数据相加-----------");
System.out.println("3.14+100="+o.add(3.14,100));
System.out.println("----------两个float数据相加-----------");
System.out.println("3.14F+7.98F="+o.add(3.14F,7.98F));
}
}
运行结果为:
----------两个int数据相加-----------
本方法的参数为两个int数据
100+200=300
----------两个double数据相加-----------
本方法的参数为两个double数据
3.14+7.98=11.120000000000001
----------两个long数据相加-----------
本方法的参数为两个long数据
300L+400L=700
----------三个int数据相加-----------
本方法的参数为三个int数据
100+200+300=600
----------一个int数据和一个double数据相加-----------
本方法的参数为一个int数据和一个double数据
100+3.14=103.14
----------一个double数据和一个int数据相加-----------
本方法的参数为一个double数据和一个int数据
3.14+100=103.14
----------两个float数据相加-----------
本方法的参数为两个double数据
3.14F+7.98F=11.120000123977661
由此可以看出,计算机可以根据参数的三个特性来识别正确的同名方法。
需要注意的是,由于自动类型转换的存在,如果实参不能精确匹配形参,那么计算机会将实参进行自动类型转换,以便尽量匹配已有的方法——除非进行自动类型转换之后也找不到合适的方法,就会在编译时提示程序出错。
在示例中最后一次调用add方法,就使用了两个float类型的实参,计算机找不到使用float类型数据做参数的add方法,但能找到使用double类型数据做参数的add方法,于是将两个float类型实参进行自动类型转换为double类型,进而调用已有的方法。而如果能找到直接匹配的方法,就不会进行自动类型转换。我们可以在Overload.java程序中添加方法:
// 计算两个float数据相加的方法
public float add(float x, float y) {
System.out.println("本方法的参数为两个float数据");
return x + y;
}
计算机就会调用该方法,而不是参数为double数据的方法了。
04
构造方法
在之前的程序中,我们需要在生成对象之后设置其属性值。如果属性比较少还好说,但如果一个类有很多属性,那可就麻烦了,不仅一个一个设置很麻烦,还很容易有遗漏。虽然生成对象时,所有的属性被赋予默认的值了,但绝大多数情况下,默认值其实就是不符合我们需要的值,仅仅是语法上不出错而已。如果有办法能够一次性给多个属性赋值,而且强制程序员必须给这些属性赋值就好了。
答案是:使用构造方法。
构造方法是一种特殊的方法,专门用于初始化对象时为属性赋值,或执行其他的初始化操作。构造方法有三个特点:
- 构造方法的名字和类名完全一致
- 构造方法没有返回值类型,连
void
都不用写 - 构造方法不能用
return
语句返回一个值,但可以用return语句结束方法
构造方法也可以重载。
我们来看一个例子:
public class Person {
//年龄属性
byte age;
//姓名属性
String name;
}
等等,构造方法呢?这就是我们要讲的第一种构造方法:默认构造方法。
默认构造方法
好了,别再看了,你是看不到它的。所谓“默认构造方法”,就是我们不写,也会存在的一个构造方法。如果写出来,它会是这个样子:
public Person() {
super();
}
当然,如果不写,它也是这个样子。要不怎么叫做“默认”构造方法呢。先不用理会那句super();
,后面会讲到(剧透一下,其实这句也是默认的,写不写都包含进来了)。现在你一定回想起之前的程序了,原来前面示例中的那句Overload o=new Overload();
中,new
后面的类名带括号部分就是默认构造方法啊。
那么这个构造方法能做什么呢?用默认值为各个属性赋值,换言之,生成一个毫无特色的对象。
使用默认构造方法生成对象后,我们还是需要为每个属性赋值,开始时提到的问题并没有得到解决。所以,我们需要自定义构造方法。
自定义构造方法
顾名思义,自定义构造方法就是根据需要,我们自己编写的构造方法。
需要注意的是:我们自己编写构造方法后,默认构造方法将不再存在。我们可以理解为:计算机发现我们没有定义构造方法,就给我们一个默认构造方法;计算机发现我们已经定义了构造方法,就把默认构造方法收走了。
在自定义构造方法时,我们可以根据需要为若干个属性赋值——因此,我们可以定义多个构造方法。因为构造方法也可以重载。
我们现在为前面的Person类编写构造方法:
//无参构造方法
public Person() {
}
//为age属性赋值的构造方法
public Person(byte a) {
age=a;
}
//为age和name属性赋值的构造方法
public Person(byte a,String n) {
age=a;
name=n;
}
这样,当我们创建一个Person类型对象时,如果只想为年龄赋值,则可以写成:
Person p1=new Person((byte)30);
如果直接用30
做参数,计算机会认为这是一个int类型的数据,所以要强制类型转换为byte类型,以便和定义方法时参数类型一致。
this关键字
在IDE中,我们一般不会手写构造方法,我们会在代码编辑区单击鼠标右键,在弹出菜单中选择Source–>Generate Constructor using Fields,然后在对话框中选择要处理的属性,单击Generate按钮即可自动生成。看代码:
//无参构造方法
public Person() {
super();
}
//为age属性赋值的构造方法
public Person(byte age) {
super();
this.age = age;
}
//为所有属性赋值的构造方法
public Person(byte age, String name) {
this.age = age;
this.name = name;
}
如果不想看到super();
,在生成构造方法的对话框中,选择Omit call to default construtor super()
就可以了,第三个构造方法就是这样生成的。
在这里,我们注意到,构造方法的参数名和属性名是相同的,而且还出现了this关键字。
之所以将构造方法的参数名和对应的属性名设置成一样,是为了看起来非常直观,一眼就能看出来哪个参数对应哪个属性。但问题也随之而来:按照作用域的规则,在方法中,如果参数名称和某个属性名称相同,参数有效,属性无效。为了解决这个问题,Java中设计了this
关键字。
我们可以把this理解为“当前对象”,this.age
就是“当前对象的age”,这样就可以同参数age区分开了。
除了强调某个变量是当前对象的属性(成员变量),还可以使用this调用当前对象中的方法——不过意义不大,因为我们不能在方法里面再定义方法,所以一般会直接通过方法名调用对应的方法,而不是加上this。
this的另一个作用就是调用当前对象的构造方法。例如调用当前对象中的无参构造方法:this();
。有了这个机制,我们就可以把前面示例中的第三个构造方法改写成:
//为所有属性赋值的构造方法
public Person(byte age, String name) {
this(age);
this.name = name;
}
注意,在构造方法中调用其它构造方法,只能使用this关键,如果写成Person(age);
,计算机会提示有错误。而且使用this关键字调用构造方法时,必须写到构造方法第一行。这是一个很有意思的规定,因为super();
也必须写到第一行……所以,在Java中,super();
是默认的,即使不写也会存在。
关于super
关键字,我们在后面会加以说明。
04
static关键字和代码块
Java中还有一个很重要的关键字,static
,翻译过来就是静态的。它可以修饰类的成员变量(属性)、成员方法和代码块。
代码块是新概念,我们就先说它吧。
代码块和静态代码块
把一段代码,用{}
括起来,就是代码块了。根据出现的位置和是否使用static
修饰,代码块分为普通代码块、构造代码块和静态代码块三种(实际上还有一种同步代码块,我们放到多线程那部分进行说明)。
- 普通代码块:写在成员方法中,对执行顺序没有影响,但会影响变量的生命周期;
- 构造代码块:写在类中,在创建对象时,会早于构造方法执行;
- 静态代码块:写在类中,并且用static修饰,在加载类时执行且执行一次。
看代码CodeBlock01.java
:
public class CodeBlock01 {
//属性x
int x;
//成员方法
public void show(){
//普通代码块
{
int a=30;
System.out.print("这里是普通代码块:");
System.out.println("a="+a);
}
int a=50;
System.out.print("这里是普通方法:");
System.out.println("a="+a);
}
//构造代码块
{
x=100;
System.out.println("这里是构造代码块,x="+x);
}
public CodeBlock01(int x) {
this.x = x;
System.out.println("这里是构造方法,x="+x);
}
//静态代码块
static {
System.out.println("这里是静态代码块");
}
}
编写代码CodeBlock01Main.java
调用CodeBlock01
类:
public class CodeBlock01Mian {
public static void main(String[] args) {
CodeBlock01 cb01=new CodeBlock01(1000);
cb01.show();
CodeBlock01 cb02=new CodeBlock01(2000);
cb02.show();
}
}
运行结果为:
这里是静态代码块
这里是构造代码块,x=100
这里是构造方法,x=1000
这里是普通代码块:a=30
这里是普通方法:a=50
这里是构造代码块,x=100
这里是构造方法,x=2000
这里是普通代码块:a=30
这里是普通方法:a=50
我们可以发现:
- 静态代码块只会在类加载时执行一次,即使多次使用到该类,也只执行一次;
- 构造代码块会在每次调用构造方法前调用一次
- 普通代码块结束,其中的变量的作用域也结束
需要注意的是,如果我们在本例中的普通代码块之前定义变量a,则会出现冲突,这是由于:普通代码块可以使用外界的变量,但普通代码块之外不能访问普通代码块内的变量。就像单向透光的太阳镜一样。
静态变量和静态方法
用static关键字修饰一个类的成员变量,这就是静态变量;用static关键字修饰一个类的成员方法,这就是静态方法。
静态变量和静态方法都可以通过类名直接访问。
来看程序CodeBlock02.java
:
public class CodeBlock02 {
//普通成员变量
int a;
//静态成员变量
static int b;
//普通方法
public void m1(){
System.out.println("这里是普通方法");
}
//静态方法
public static void m2(){
System.out.println("这里是静态方法");
}
}
编写代码CodeBlock02Main.java
调用CodeBlock02
类:
public class CodeBlock02Main {
public static void main(String[] args) {
CodeBlock02 cb01=new CodeBlock02();
cb01.a=10;
//通过对象修改静态变量b的值
cb01.b=100;
CodeBlock02 cb02=new CodeBlock02();
cb02.a=20;
//通过对象修改静态变量b的值
cb02.b=200;
//通过类修改静态变量b的值
CodeBlock02.b=300;
System.out.println("cb01:a="+cb01.a+",b="+cb01.b);
System.out.println("cb02:a="+cb02.a+",b="+cb02.b);
cb01.m1();
//通过变量调用静态方法m2
cb01.m2();
//通过类调用静态方法m2
CodeBlock02.m2();
}
}
运行结果为:
cb01:a=10,b=300
cb02:a=20,b=300
这里是普通方法
这里是静态方法
这里是静态方法
我们可以发现:
- 静态变量和静态方法都可以直接通过类来调用
- 我们通过两个对象和类修改了静态变量的值,但运行结果却显示了最后一次修改的值,说明一个类所有的对象会共用静态变量。
这是由于,静态的内容(包括静态变量、静态方法和静态代码块)在类加载后,是单独存放的,所以静态代码块会在类加载时执行一次,而且只执行一次(因为在一次运行中类不会重复加载),所有的对象会共用相同的静态属性;也正是由于它们是单独存放的,所以不需要对象也可以通过类直接调用静态变量和静态方法。
但也由于它们是单独存放的,静态代码块只能调用静态变量和静态方法,静态方法只能调用静态变量或其它静态方法。但是普通方法可以调用静态变量和静态方法。
04