定义和使用类

我们不是学过了吗?

看到标题,很多人可能会想:我们之前不是讲过如何定义和使用类了吗?
是的,我们讲过了,现在我们来回顾一下。
定义类:

class 类名{
    //使用变量和对象表示属性
    数据类型 变量名/对象名;
    //使用方法来表示行为
    方法返回值类型 方法名(参数列表){
        方法体;
    }
}

生成和使用对象的方法是:

类名 对象名=new 类名();
对象名.属性名……
对象名.方法名……

今后,我们还会学习更多的知识。为了方便说明,今后,我们把定义在方法中的变量叫做局部变量;定义在类中的变量,也就是属性,称为成员变量;定义在类中的方法,也就是行为,称为成员方法。

封装

我们之前设定对象属性时,会使用这种形式:对象名.属性名=属性值;。但这种形式有个缺陷:我们可以为某个属性设置符合其数据类型表达范围的任意值。换言之,我们可能将某个属性的值设置为语法正确但含义错误。
例如,某个类中包含年龄属性,无论使用整数类型还是实数类型,我们都可以把年龄设置为负数!在语法上,这是完全允许的,但现实中,没有人的年龄是负数。
因此,为了避免出现这种问题,我们可以不允许程序员直接访问这些属性,同时设计一些方法,以便程序员调用并设置属性值和获取属性值。我们可以在这些方法中做一些设置,从而减少出错的可能性。此外,我们也可以在设置属性和获取属性值的方法设置其他功能,例如MAC地址、IP地址格式的转换。
这个处理思路就叫做封装
封装的实施步骤有两个:

  1. 将属性隐藏在类内部,使用private修饰属性即可;
  2. 为被封装的属性设置对应的设置值和获取值的方法
  3. 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

构造方法

在之前的程序中,我们需要在生成对象之后设置其属性值。如果属性比较少还好说,但如果一个类有很多属性,那可就麻烦了,不仅一个一个设置很麻烦,还很容易有遗漏。虽然生成对象时,所有的属性被赋予默认的值了,但绝大多数情况下,默认值其实就是不符合我们需要的值,仅仅是语法上不出错而已。如果有办法能够一次性给多个属性赋值,而且强制程序员必须给这些属性赋值就好了。
答案是:使用构造方法。
构造方法是一种特殊的方法,专门用于初始化对象时为属性赋值,或执行其他的初始化操作。构造方法有三个特点:

  1. 构造方法的名字和类名完全一致
  2. 构造方法没有返回值类型,连void都不用写
  3. 构造方法不能用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

我们可以发现:

  1. 静态代码块只会在类加载时执行一次,即使多次使用到该类,也只执行一次;
  2. 构造代码块会在每次调用构造方法前调用一次
  3. 普通代码块结束,其中的变量的作用域也结束

需要注意的是,如果我们在本例中的普通代码块之前定义变量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
这里是普通方法
这里是静态方法
这里是静态方法

我们可以发现:

  1. 静态变量和静态方法都可以直接通过类来调用
  2. 我们通过两个对象和类修改了静态变量的值,但运行结果却显示了最后一次修改的值,说明一个类所有的对象会共用静态变量。

这是由于,静态的内容(包括静态变量、静态方法和静态代码块)在类加载后,是单独存放的,所以静态代码块会在类加载时执行一次,而且只执行一次(因为在一次运行中类不会重复加载),所有的对象会共用相同的静态属性;也正是由于它们是单独存放的,所以不需要对象也可以通过类直接调用静态变量和静态方法。
但也由于它们是单独存放的,静态代码块只能调用静态变量和静态方法,静态方法只能调用静态变量或其它静态方法。但是普通方法可以调用静态变量和静态方法。

04

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值