前言
本文主要介绍了成员方法和面向对象的特性之一封装,主要包括构造方法、方法重载、this关键字、方法的递归调用和封装,包含了概念基础和应用举例,适合初学者。
1.构造方法
(1)构造方法的概念和应用
格式为:
class 类名 {
类名(形参列表) {
构造方法体;
}
}
构造方法的特点为:
方法名与类名相同;
没有返回值类型,也不需要void类型。
如下:
public class MethodPerson {
String name;
int age;
MethodPerson() {
System.out.println("执行了自定义的构造方法");
}
void printVariable() {
System.out.println("我是" + name + ",今年" + age + "岁了");
}
public static void main(String[] args) {
MethodPerson p = new MethodPerson();
p.printVariable();
}
}
输出:
执行了自定义的构造方法
我是null,今年0岁了
可以看到,调用了定义的构造方法Person() ,但是我们之前的例子中都没有定义构造方法,也能正常运行,这是因为当类中没有定义构造方法时,编译器会自动添加无参空构造构造方法,即默认构造方法、缺省构造方法,如Person(){} ;
若类中提供了构造方法,则编译器不再提供构造方法。
(2)构造方法的作用
如果想要在构建对象的同时,自定义成员变量的初始值,就需要自定义构造方法;
如果需要在创建不同对象的同时,其成员变量的值不同,就可以传递参数。
如下:
public class MethodPerson {
String name;
int age;
MethodPerson(String s, int i) {
System.out.println("执行了" + s + "的构造方法");
name = s;
age = i;
}
MethodPerson() {
}
void printVariable() {
System.out.println("我是" + name + ",今年" + age + "岁了");
}
public static void main(String[] args) {
MethodPerson p1 = new MethodPerson("Corley", 18);
p1.printVariable();
MethodPerson p2 = new MethodPerson("Jack", 20);
p2.printVariable();
MethodPerson p3 = new MethodPerson();
p3.printVariable();
}
}
输出:
执行了Corley的构造方法
我是Corley,今年18岁了
执行了Jack的构造方法
我是Jack,今年20岁了
我是null,今年0岁了
可以看到,可以同时定义有参和无参的构造方法,便于在创建对象时传递或者不传递参数都可以成功创建对象。
可以总结:
使用new关键字创建对象时会自动 调用构造方法实现成员变量的初始化。
再在Point类中实现构造方法,如下:
public class MethodPoint {
int x;
int y;
MethodPoint() {}
MethodPoint(int i, int j) {
x = i;
y = j;
}
void show() {
System.out.println("横坐标是" + x + ",纵坐标是" + y);
}
public static void main(String[] args) {
MethodPoint p1 = new MethodPoint();
p1.show();
MethodPoint p2 = new MethodPoint(12, 20);
p2.show();
}
}
输出:
横坐标是0,纵坐标是0
横坐标是12,纵坐标是20
2.方法重载
(1)重载的概念和体现形式
方法名称相同、参数列表不同,则这些方法之间构成重载(Overload) 关系。
示意如下:
public class OverloadTest {
void show() {
System.out.println("Showing...");
}
void show(int i) {
System.out.println("Showing " + i);
}
void show(int i, double d) {
System.out.println("Showing " + i + " " + d);
}
void show(int i, int j) {
System.out.println("Showing " + i + " " + j);
}
void show(double d, int i) {
System.out.println("Showing " + i + " " + d);
}
public static void main(String[] args) {
OverloadTest ot = new OverloadTest();
ot.show();
ot.show(123);
ot.show(123, 123.456);
ot.show(123, 456);
ot.show(123.456, 123);
}
}
输出:
Showing...
Showing 123
Showing 123 123.456
Showing 123 456
Showing 123 123.456
方法重载的体现形式总结如下:
-
方法参数的个数不同 -
方法参数的类型不同 -
方法参数的顺序不同 -
与返回值类型和形参变量名无关
????建议返回值类型相同
判断方法能否构成重载的核心∶
调用方法时能否加以区分 。
(2)方法重载的简单应用
MethodPerson类中实现方法重载,如下:
public class MethodPerson {
String name;
int age;
MethodPerson(String s, int i) {
System.out.println("执行了" + s + "的构造方法");
name = s;
age = i;
}
MethodPerson() {
}
void grow() {
age++;
}
void grow(int a) {
age += a;
}
void printVariable() {
System.out.println("我是" + name + ",今年" + age + "岁了");
}
public static void main(String[] args) {
MethodPerson p1 = new MethodPerson("Corley", 18);
p1.printVariable();
MethodPerson p2 = new MethodPerson("Jack", 20);
p2.printVariable();
MethodPerson p3 = new MethodPerson();
p3.printVariable();
System.out.println("----------------------------------------------");
p2.grow();
p2.printVariable();
p2.grow(5);
p2.printVariable();
}
}
输出:
执行了Corley的构造方法
我是Corley,今年18岁了
执行了Jack的构造方法
我是Jack,今年20岁了
我是null,今年0岁了
----------------------------------------------
我是Jack,今年21岁了
我是Jack,今年26岁了
MethodPoint类中实现方法重载,如下:
public class MethodPoint {
int x;
int y;
MethodPoint() {}
MethodPoint(int i, int j) {
x = i;
y = j;
}
void show() {
System.out.println("横坐标是" + x + ",纵坐标是" + y);
}
void down() {
y--;
}
void down(int dy) {
y -= dy;
}
public static void main(String[] args) {
MethodPoint p1 = new MethodPoint();
p1.show();
MethodPoint p2 = new MethodPoint(12, 20);
p2.show();
System.out.println("-----------------------------------");
p2.down();
p2.show();
p2.down(5);
p2.show();
}
}
输出:
横坐标是0,纵坐标是0
横坐标是12,纵坐标是20
-----------------------------------
横坐标是12,纵坐标是19
横坐标是12,纵坐标是14
(3)重载的实际意义
方法重载的实际意义在于只需要记住一个方法名就可以调用各种不同的版本、实现不同的功能。
例如对于我们经常用的打印的方法java.io.FilterOutputStream.println() 就实现了方法的重载,所以可以打印各种类型的变量,如下:
方法 | 含义 |
---|
println() | 通过写行分隔符字符串来终止当前行。 | println?(boolean x) | 打印一个布尔值,然后终止该行。 | println?(char x) | 打印一个字符,然后终止该行。 | println?(char[] x) | 打印一个字符数组,然后终止该行。 | println?(double x) | 打印一个双,然后终止该行。 | println?(float x) | 打印一个浮点数,然后终止该行。 | println?(int x) | 打印一个整数,然后终止该行。 | println?(long x) | 打印一个长,然后终止该行。 | println?(Object x) | 打印一个对象,然后终止该行。 | println?(String x) | 打印一个字符串,然后终止该行。 |
方法重载的使用场景:
如果需要定义构造方法,需要使用方法重载;
如果需要定义功能类似、但是参数可能不同的方法,需要使用方法重载。
3.this关键字
(1)this关键字的基本概念
this关键字的含义:
public class ThisTest {
ThisTest() {
System.out.println("构造方法中,this = " + this);
}
void print() {
System.out.println("成员方法中,this = " + this);
}
public static void main(String[] args) {
ThisTest tt = new ThisTest();
System.out.println("main方法中,tt = " + tt);
tt.print();
}
}
输出:
构造方法中,this = ThisTest@50eac852
main方法中,tt = ThisTest@50eac852
成员方法中,this = ThisTest@50eac852
可以看到,在构造方法中的this所代表的对象和new 得到的对象是同一个对象。
(2)this关键字的工作原理
this关键字是当前类类型的引用,也是构造方法和成员方法中隐含的关键字。
之前在成员方法和构造方法中使用成员变量时,直接使用了成员变量名,与main方法中引用.成员变量名 的形式有所区别,例如System.out.println("我是" + name + ", 年龄是" + age + "岁了"); ,这是因为隐藏了关键字this,等价于System.out.println("我是" + ``this.name`` + ", 年龄是" + this.age + "岁了"); ,执行效果是相同的。
this关键字的工作原理如下:
在构造方法中和成员方法中访问成员变量时,编译器会加上this的前缀,而this. 相当于汉语中"我的" ,当不同的对象调用同一个方法时,由于调用方法的对象不同导致this关键字所代表的对象随之不同,从而this. 方式访问的结果也就随之不同。
(3)this关键字的使用方式
Ⅰ 使用方式一
之前在定义成员方法时,一般都是保证形参变量名与成员变量不同。但是为了增加代码的可读性,还是应该让形参变量名与成员变量保持一致。
由于就近原则,当局部变量名与成员变量名相同时,在方法体中会优先使用较近的局部变量(形参变量)、而不是成员变量,若希望使用成员变量,则可以在成员变量的前面加上this前缀,明确声明该变量是成员变量。
MethodPerson类使用this进行修改如下:
public class MethodPerson {
String name;
int age;
MethodPerson(String name, int age) {
System.out.println("执行了" + name + "的构造方法");
this.name = name;
this.age = age;
}
MethodPerson() {
}
void grow() {
age++;
}
void grow(int age) {
this.age += age;
}
void printVariable() {
System.out.println("我是" + this.name + ", 年龄是" + age + "岁了");
}
public static void main(String[] args) {
MethodPerson p1 = new MethodPerson("Corley", 18);
p1.printVariable();
MethodPerson p2 = new MethodPerson("Jack", 20);
p2.printVariable();
MethodPerson p3 = new MethodPerson();
p3.printVariable();
System.out.println("----------------------------------------------");
p2.grow();
p2.printVariable();
p2.grow(5);
p2.printVariable();
}
}
效果与之前相同。
MethodPoint类也可以进行修改,如下:
public class MethodPoint {
int x;
int y;
MethodPoint() {}
MethodPoint(int x, int y) {
this.x = x;
this.y = y;
}
void show() {
System.out.println("横坐标是" + x + ",纵坐标是" + y);
}
void down() {
y--;
}
void down(int y) {
this.y -= y;
}
public static void main(String[] args) {
MethodPoint p1 = new MethodPoint();
p1.show();
MethodPoint p2 = new MethodPoint(12, 20);
p2.show();
System.out.println("-----------------------------------");
p2.down();
p2.show();
p2.down(5);
p2.show();
}
}
Ⅱ 使用方式二
this关键字除了可以通过this. 的方式调用成员变量和成员方法,还可以作为方法的返回值 。
public class MethodPerson {
String name;
int age;
MethodPerson(String name, int age) {
System.out.println("执行了" + name + "的构造方法");
this.name = name;
this.age = age;
}
MethodPerson() {
}
void grow() {
age++;
}
void grow(int age) {
this.age += age;
}
MethodPerson getPerson() {
return this;
}
void printVariable() {
this.grow();
System.out.println("我是" + this.name + ", 年龄是" + age + "岁了");
}
public static void main(String[] args) {
MethodPerson p1 = new MethodPerson("Corley", 18);
p1.printVariable();
MethodPerson p2 = new MethodPerson("Jack", 20);
p2.printVariable();
MethodPerson p3 = new MethodPerson();
p3.printVariable();
System.out.println("----------------------------------------------");
p2.grow();
p2.printVariable();
p2.grow(5);
p2.printVariable();
System.out.println("----------------------------------------------");
MethodPerson p4 = p1.getPerson();
System.out.println("p1 = " + p1);
System.out.println("p4 = " + p4);
}
}
输出:
执行了Corley的构造方法
我是Corley, 年龄是19岁了
执行了Jack的构造方法
我是Jack, 年龄是21岁了
我是null, 年龄是1岁了
----------------------------------------------
我是Jack, 年龄是23岁了
我是Jack, 年龄是29岁了
----------------------------------------------
p1 = MethodPerson@2a3b5b47
p4 = MethodPerson@2a3b5b47
Ⅲ 使用方式三
在构造方法的第一行 可以使用this() 的方式来调用本类中的其他构造方法 。
public class Boy {
String name;
Boy() {
this("Jack");
System.out.println("无参构造方法...");
}
Boy(String name) {
System.out.println("有参构造方法...");
this.name = name;
}
void print() {
System.out.println("我的名字是" + name);
}
public static void main(String[] args) {
Boy b1 = new Boy();
b1.print();
System.out.println("-----------------------------------");
Boy b2 = new Boy("Corley");
b2.print();
}
}
输出:
有参构造方法...
无参构造方法...
我的名字是Jack
-----------------------------------
有参构造方法...
我的名字是Corley
还可以如下:
public class Boy {
String name;
Boy() {
System.out.println("无参构造方法...");
}
Boy(String name) {
this();
System.out.println("有参构造方法...");
this.name = name;
}
void print() {
System.out.println("我的名字是" + name);
}
public static void main(String[] args) {
Boy b1 = new Boy();
b1.print();
System.out.println("-----------------------------------");
Boy b2 = new Boy("Corley");
b2.print();
}
}
输出:
无参构造方法...
我的名字是null
-----------------------------------
无参构造方法...
有参构造方法...
我的名字是Corley
但是两个构造方法中不能都调用this() ,因为这样会出现互相调用,即递归调用 ,编译不能通过。
(4)引用类型变量的注意事项
引用类型变量用于存放对象的地址,可以给引用类型赋值为null,表示不指向任何对象。 当某个引用类型变量为null时无法对对象实施访问,因为它没有指向任何对象。此时,如果通过引用访问成员变量或调用方法,会产生NullPointerException 异常。
例如:
public class Boy {
String name;
Boy() {
System.out.println("无参构造方法...");
}
Boy(String name) {
this();
System.out.println("有参构造方法...");
this.name = name;
}
void print() {
System.out.println("我的名字是" + name);
}
public static void main(String[] args) {
Boy b1 = new Boy();
b1.print();
System.out.println("-----------------------------------");
Boy b2 = new Boy("Corley");
b2.print();
Boy b3 = null;
b3.print();
}
}
可以正常编译,但是解释执行时会抛出异常Exception in thread "main" java.lang.NullPointerException 。
声明引用类型的对象时,需要有明确的指向,即不能为空,因为如果为空说明该引用在栈区的内容为空,即没有指向的堆区空间,即该对象没有对应的数据空间,所以在调用方法或者访问成员变量时就会抛出异常。
4.方法递归调用
(1)阶乘的计算
编程实现参数n的阶乘并返回,所谓阶乘就是从1累乘到n的结果。
方式一:
通过for循环实现,即递推方式 。如下:
public class FactorialTest {
int fact(int n) {
int res = 1;
for(int i = 1; i <= n; i++) {
res *= i;
}
return res;
}
public static void main(String[] args) {
FactorialTest ft = new FactorialTest();
int res = ft.fact(10);
System.out.println("计算结果是:" + res);
}
}
输出:
计算结果是:3628800
方式二:
根据阶乘的特点进行分析:
当n的值为1时,则阶乘的结果为1;
当n大于1时,n! = n * (n-1)! 。
即可以使用递归方式 ,代码如下:
public class FactorialTest {
int fact1(int n) {
int res = 1;
for(int i = 1; i <= n; i++) {
res *= i;
}
return res;
}
int fact2(int n) {
if (1 == n) return 1;
return n * fact2(n-1);
}
public static void main(String[] args) {
FactorialTest ft = new FactorialTest();
int res1 = ft.fact1(10);
System.out.println("递推方式计算结果是:" + res1);
int res2 = ft.fact2(10);
System.out.println("递归方式计算结果是:" + res2);
}
}
输出:
递推方式计算结果是:3628800
递归方式计算结果是:3628800
可以看到,执行结果完全相同,但是递归方式代码更简洁。
(2)递归方式的本质和注意事项
递归的本质是在方法体的内部直接或间接调用当前方法自身 。
使用递归的注意事项包括:
-
使用递归必须要有递归的规律和终止条件 ; -
使用递归必须使得问题简单化而不是复杂化; -
如果递归影响到程序的执行性能,则使用递推代之。
(3)斐波拉契数列的递归实现
编程实现斐波拉契数列中第n项的数值并返回。 斐波拉契数列: 1 1 2 3 5 8 13 21 ……
先用递归方式,进行分析:
如果n为1或者2,结果是1;
否则结果是前两项的和。
实现如下:
public class FibonacciSequence {
int fib(int n) {
if(1 == n || 2 == n) return 1;
return fib(n-1) + fib(n-2);
}
public static void main(String[] args) {
FibonacciSequence fs = new FibonacciSequence();
int res = fs.fib(10);
System.out.println("计算结果是:" + res);
}
}
输出:
计算结果是:55
分析递归的过程可以发现,这种方式实现斐波拉契数列的性能较低,如果n较大时就会运行很长时间,因为拆数和调用重复方法的次数较多,出现了大量计算(包括重复计算)。
现使用递推方式实现,如下:
public class FibonacciSequence {
int fib1(int n) {
if(1 == n || 2 == n) return 1;
return fib1(n-1) + fib1(n-2);
}
int fib2(int n) {
int x = 1;
int y = 1;
for(int i = 3; i <= n; i++) {
int z = x + y;
x = y;
y = z;
}
return y;
}
public static void main(String[] args) {
FibonacciSequence fs = new FibonacciSequence();
int res1 = fs.fib1(10);
System.out.println("递归计算结果是:" + res1);
int res2 = fs.fib2(10);
System.out.println("递推计算结果是:" + res2);
}
}
输出:
递归计算结果是:55
递推计算结果是:55
5.封装
(1)代码的拆分实现
在开发中,不建议将成员方法和main方法放在同一个类中,因为main方法不是这个类独有的,这样会显得代码结构不够明确、清晰,应该在实现功能时创建功能类 ,在这个类中只放这个类的成员变量和成员方法,而将main方法放到测试类 (类名一般以Test结尾)中,即将代码实现物理上的拆分 、结构更清晰、可维护性更高,同时可以实现代码的复用 。
以上面的FibonacciSequence 类为例,进行拆分,功能类FibonacciSequence.java如下:
public class FibonacciSequence {
int fib1(int n) {
if(1 == n || 2 == n) return 1;
return fib1(n-1) + fib1(n-2);
}
int fib2(int n) {
int x = 1;
int y = 1;
for(int i = 3; i <= n; i++) {
int z = x + y;
x = y;
y = z;
}
return y;
}
}
测试类FibonacciSequenceTest.java如下:
public class FibonacciSequenceTest {
public static void main(String[] args) {
FibonacciSequence fs = new FibonacciSequence();
int res1 = fs.fib1(10);
System.out.println("递归计算结果是:" + res1);
int res2 = fs.fib2(10);
System.out.println("递推计算结果是:" + res2);
}
}
此时需要先编译、再执行,不能直接java filename 来运行。
先编译javac FibonacciSequenceTest.java ,会同时生成FibonacciSequence.java 和FibonacciSequenceTest.java 的字节码文件,再执行java FibonacciSequenceTest 。
(2)封装的概念
封装是面向对象的一大特性。
通常情况下可以在测试类中给成员变量赋值一些合法但不合理的数值,无论是编译阶段还是运行阶段都不会报错或者给出提示,但是会与现实生活中的情况不符。
新建Student功能类:
public class Student {
int id;
String name;
void print() {
System.out.println("我是" + name + ",我的学号是" + id);
}
}
再建测试类:
public class StudentTest {
public static void main(String[] args) {
Student stu1 = new Student();
Student stu2 = new Student();
stu1.id = 12321;
stu1.name = "Corley";
stu1.print();
stu2.id = -12321;
stu2.name = "Jack";
stu2.print();
}
}
输出:
我是Corley,我的学号是12321
我是Jack,我的学号是-12321
可以看到,在给stu2进行赋值时,id为负值,这符合Java中整型的标准,即是合法的,但是与现实生活中的情况并不相符,因为没有学号是负值。
为了避免上述错误的发生,就需要对成员变量进行密封包装处理,来隐藏成员变量的细节 以及保证成员变量数值的合理性 ,该机制就叫做封装 。
(3)封装的实现
封装的实现步骤如下:
- 私有化成员变量,使用private 关键字修饰。
????private关键字修饰表示私有的含义,也就是该成员变量只能在当前类的内部使用 。
????之前访问成员变量的方式失效,例如stu2.id`` = -12321; 就不能再正常编译通过。
- 提供公有的get和set方法,并在方法体中进行合理值的判断。
????使用public关键字修饰,该方法可以在任意位置使用 。
????没有修饰符的方法和变量是默认的访问权限,级别介于private和public之间。
- 在构造方法中调用set方法进行合理值的判断。
Student类如下:
public class Student {
private int id;
private String name;
public Student() {}
public Student(int id, String name) {
setId(id);
setName(name);
}
public int getId() {
return id;
}
public void setId(int id) {
if (id > 0) {
this.id = id;
} else {
System.out.println("学号不合理!!!");
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void print() {
System.out.println("我是" + getName() + ",我的学号是" + getId());
}
}
StudentTest类如下:
public class StudentTest {
public static void main(String[] args) {
Student stu1 = new Student();
Student stu2 = new Student();
stu1.setId(12321);
stu1.setName("Corley");
stu1.print();
System.out.println("-------------------------------------------");
stu2.setId(-12321);
stu2.setName("Jack");
stu2.print();
System.out.println("-------------------------------------------");
Student stu3 = new Student(-45654, "Bob");
stu3.print();
}
}
输出:
我是Corley,我的学号是12321
-------------------------------------------
学号不合理!!!
我是Jack,我的学号是0
-------------------------------------------
学号不合理!!!
我是Bob,我的学号是0
此时Student类就是封装类。
(4)封装类实现学生信息的录入
提示用户输入班级的学生人数以及每个学生的信息,学生的信息有:学号、姓名,最后分别打印出来。 提示:Student[] arr = new Student[num];
Student类使用前面定义的Student类,StudentTest类如下:
import java.util.Scanner;
public class StudentTest {
public static void main(String[] args) {
System.out.println("请输入学生人数:");
Scanner sc = new Scanner(System.in);
int count = sc.nextInt();
Student[] stus = new Student[count];
for (int i = 0; i < count; i++) {
System.out.println("请输入第" + (i + 1) + "个学生的信息(学号 姓名):");
stus[i] = new Student(sc.nextInt(), sc.next());
}
System.out.println("所有学生的信息为:");
for(int i = 0; i < count; i++) {
stus[i].print();
}
}
}
输出:
请输入学生人数:
3
请输入第1个学生的信息(学号 姓名):
12321 Corley
请输入第2个学生的信息(学号 姓名):
12322 Jack
请输入第3个学生的信息(学号 姓名):
12323 Bob
所有学生的信息为:
我是Corley,我的学号是12321
我是Jack,我的学号是12322
我是Bob,我的学号是12323
其中,Student[] stus = new Student[count]; 的含义是:
数组中的每个元素都是Student类型,即数组中的每个元素都可以看做Student类型的变量,所以对每个元素进行初始化时可以采用arr[i] = new Student(); 的方式。
(5)JavaBean的概念
JavaBean是一种Java语言写成的可重用组件,其他Java 类可以通过反射机制发现和操作这些JavaBean 的属性。 JavaBean本质上就是符合以下标准的Java类:
-
类是公共的 -
有一个无参的公共的构造器 -
有属性,且有对应的get、set方法
可以看到,JavaBean其实就是一个封装类。
总结
方法是一个类实现一定功能的基础,同时方法具有重载、递归等特性;作为面向对象的三大特征之一,封装将对象内部的成员封闭起来,不对外开放,同时增加了对用户合理性的判断,防止了用户的恶意操作,从而保证了对象的完整性。
|