3. Java-面向对象(上)
面向对象-上
每天学习新知识,每天进步一点点。
1. 面向对象概述
面向对象是一种符合人类思维习惯的编程思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系,在程序中使用对象来映射现实中的事物,使用对象的关系来描述事物之间的联系,这种思想就是面向对象。
面向对象(Object-Oriented Programming,OOP)是一种编程范式,它将程序的运行和数据表示为一系列对象,每个对象都有自己的属性和方法。
2. 面向对象编程的主要特征
封装(Encapsulation)是面向对象的核心思想,将对象的属性和行为封装起来,不需要让外界知道具体实现细节,这就是封装思想。(将对象的状态(字段)私有化,通过公共方法访问。)继承(Inheritance)主要描述的就是类与类之间的关系,通过继承,可以在无需重新编写原有类代码的基础下,对原有类的功能进行扩展。例如:有一个汽车的类,该类中描述了汽车普通属性(轮胎,颜色等)和功能,而汽车也分为很多种,轿车,货车。那轿车、货车的类中不仅应该包含汽车类的属性和功能,还应该增加自己特有的属性和功能。这时我们可以让轿车类、货车类继承汽车类,在类中单独增加特有的属性和方法。不仅增强了代码的可用性,还提高了开发效率,为程序维护提供便利。(一个类可以继承另一个类的属性和方法。)多态(Polymorphism)指的是在一个类中定义的属性和功能被其他类继承后,当把子类对象直接赋值给父类引用变量时,相同引用类型的变量调用同一个方法所呈现的多种不同的行为特征。多态是面向对象编程的核心特征,它使程序具有灵活性、扩展性和可维护性。(主要通过方法重载和方法重写实现。)
3. Java中的类与对象
3.1 类与对象的关系
面向对象的思想中提出了两个概念--
类和对象.其中,类是对某一事物的抽象描述,而对象用于表示现实中该类事物的个体。如例:可以将人看作是一个类,将每个具体的人都看作是一个对象。从人与具体个人之间的关系便可以知道类和对象之间的关系。类用于描述多个对象的特征,它是对象的模板,而对象用于描述现实中的个体,是类的实例。
3.2 类的定义
3.2.1 类的定义格式
类是对一类事物的抽象,是一种
模板,用来创建对象。类由属性和方法组成,属性用于描述对象,方法用于实现对象功能。类可以包含属性和方法,也可以继承其他类。
Java中的类是通过class关键字来定义的,类的定义语法格式如下:
[修饰符] class 类名 [extends 父类名] [implements 接口名1, 接口名2,...]{
//类体,包括类的成员变量和成员方法
}
在上述语法格式中,class前面的修饰符可以是public,也可以不写(默认)。类名是类的名称,extends关键字用于指定父类,implements关键字用于指定实现的接口。类体包含了类的成员变量和成员方法。
3.2.2 声明(定义)成员变量
类的
成员变量也被称作类的属性,它主要用于描述对象的特征。比如:一个人的基本属性特征有年龄、职业、性别、姓名、住址等信息,在类中要想使用这些信息,就需要先将它们声明(定义)为成员变量。
声明(定义)成员变量的定义格式如下:
[访问权限修饰符] [数据类型] 变量名 [= 初始值];
在上述语法格式中,访问权限修饰符可以是public,private,protected,static,final,等。数据类型可以是Java中的任意类型。成员变量名是变量名,用于标识变量的名称,必须符合命名规则。初始值是变量的初始值,可以赋值,也可以不赋值。通常情况下,未赋值的变量称未声明变量,已赋值的变量称定义变量。例如:
// 声明一个String类型的变量name
String name;
// 定义一个int类型的age,并赋值为18
int age = 18;
3.2.3 声明(定义)成员方法
成员方法也被称为
方法,类似于C语言中的函数,主要用于描述对象的行为。一个人的基本行为特征有吃饭、睡觉、运动等。这些行为在Java类中,就可以定义成方法。
声明(定义)成员方法的定义格式如下:
[访问权限修饰符] [返回值类型] 方法名([参数类型 参数名1, 参数类型 参数名2,...]) {
//方法体
....
[return 返回值;] //当方法的返回值类型为void,return及其返回值可以省略
}
上述语法格式中,[]代表可选,各部分具体说明如下:
- 修饰符:方法的修饰符比较多,有对访问权限限定的(如:public、protected、private)、静态修饰符(static)、还有最终修饰符(final)等。
- 返回值类型:方法的返回值类型,用于限定方法
返回值的数据类型,如果不需要返回值,可以使用void关键字。 - 参数类型:用于限定
调用方法时传入参数的数据类型。 - 参数名:是一个变量,用于
接收调用方法时传入的数据。 - return关键字:用于
结束方法以及返回方法指定类型的值,当返回值类型为void时,return关键字可以省略。 - 返回值:当方法的返回值类型不是
void时,被return语句返回的值,该值会返回给调用者。
了解了类及其成员的定义方法后,通过案例来演示类的定义。如例:
public class Person {
// 定义一个String类型的变量name
String name;
// 定义一个int类型的age,并赋值为18
int age = 18;
// 定义一个方法,用于打印对象的信息
public void printInfo() {
System.out.println("姓名:" + name + ",年龄:" + age);
}
}
在上述代码中,定义了一个Person类,并在类中定义了类的成员变量和成员方法。其中Person是类名,age和name是类的成员变量,printInfo()是类的成员方法。在成员方法中可以直接访问变量age和name。
tip: 在java中,定义在类中的变量称为成员变量,定义在方法内的变量称为局部变量。如果在一个方法中定义的局部变量与成员变量同名,此时方法中通过变量名访问到的是局部变量,而不是成员变量。例如:
public class Person {
// 定义一个String类型的变量name
String name;
// 定义一个int类型的age,并赋值为18
int age = 18;
// 定义一个方法,用于打印对象的信息
public void printInfo() {
String name = "小明"; // 定义一个局部变量name
// 定一个局部变量age,并赋值为18
int age = 22;
System.out.println("姓名:" + name + ",年龄:" + age);
}
}
此时,程序就会输出姓名:小明,年龄:22,因为局部变量name和age覆盖了成员变量name和age,所以程序输出的是局部变量的值。
3.3 对象的创建与使用
应用程序想要完成具体的功能,仅有类是不够的,还需要根据类创建实例对象。在java中,可以使用
new关键字来创建对象。
new关键字创建对象的语法格式如下:
类名 对象名 = new 类名();
例如:
Person p = new Person();
在上述代码中,Person是类名,p是对象名,new Person()是创建Person类的一个实例对象,Person p则是声明了一个Person类型的变量p。中间的等号用于将Person对象在内存中的地址赋值给变量p,这样变量p便持有了对象的引用。
在创建
Person对象时,程序会占用两块内存区域,分别是堆内存和栈内存。其中Person类型的变量p被存放在栈内存中,它是一个引用,会指向真正的对象,通过new Person()创建的对象放在堆内存中,才是真正的对象。
tip:Java将内存分为两种,即栈内存和堆内存。其中栈内存用于存放基本类型的变量和对象的引用变量(如 Person p),堆内存则用于存放new创建的对象和数组。
创建对象后,可以通过对象调用其成员方法,也可以通过对象访问其成员变量。具体格式如下:
对象名.成员变量名
对象名.成员方法名()
接下来,通过一个案例来学习如何创建对象并调用其成员方法(需要使用到上面的Person类)。如例:
public class Demo1 {
public static void main(String[] args) {
// 创建第一个Person对象
Person p1 = new Person();
// 创建第二个Person对象
Person p2 = new Person();
// 设置对象的name属性
p1.name = "小明";
p2.name = "小红";
p2.age = 20;
// 调用对象的printInfo方法
p1.printInfo();
p2.printInfo();
}
}
输出
姓名:小明,年龄:18
姓名:小红,年龄:20
在上述代码中,创建了两个Person对象,p1、p2分别引用了Person类的两个实例对象。p1和p2在调用printInfo()方法时,打印的name和age属性值不同。这是因为p1对象和p2对象是两个独立的个体,分别拥有各自的属性,对p2对象的赋值不会影响p1对象属性的值。
tip:除了使用Person p = new Person()创建对象外,还可以直接使用创建的对象本身来引用对象成员,如:new 类名().成员变量名。这种方法在通过new关键字创建实例对象的同时就访问了对象的成员,并且创建后只能访问一个成员,不能像对象引用那样访问多个成员。同时由于没有对象引用的存在,所以用完后就成了垃圾对象。所以实际开发中,创建实例对象时多数还是会使用对象引用。
当对象被实例化后,在程序中可以通过对象的引用变量来访问该对象的成员。需要注意的是,当没有任何变量引用这个对象时,它将会变成垃圾对象,不能再被使用。
第一段程序代码:
{
Person p1 = new Person();
...
}
上面代码中,使用变量p1引用了Person对象,当这段代码运行完毕后,变量p1就会超出其作用域而被销毁。这时Person类型的对象将会因为没有任何变量引用它而变成垃圾对象。
第二段程序代码:
{
Person p2 = new Person();
...
p2 = null; // 置空对象引用
}
上面代码中,使用变量p2引用了Person对象,当这段代码运行完毕后,接着将变量p2的值置为null,则表示该变量不指向任何一个对象,被p2引用的Person对象就会失去引用,成为垃圾对象。
3.4 访问控制符
在java中,针对
类、成员方法和属性提供了四种访问级别,分别是private、default、protected、public。(已经按照控制级别由小到大列出)
具体介绍如下:
private(当前类访问级别):这个成员只能被被类的其他成员访问,其他类无法直接访问。default(包访问级别):这个类或者类的成员只能被本包的其他类访问。protected(子类访问级别):这个成员既能被同一包下的其他类访问,也能被不同包下该类的子类访问。public(公共访问级别):这个类或者类的成员能被所有的类访问
| 访问范围 | private | default | protected | public |
|---|---|---|---|---|
| 当前类 | √ | √ | √ | √ |
| 同一包 | √ | √ | √ | |
| 子类 | √ | √ | ||
| 全局范围 | √ |
4. 类的封装
4.1 为什么需要封装?
在正式实现类的封装之前,先通过一个案例来了解为什么需要对类就行封装。
如例:
class Person{
String name;
int age;
public void printInfo(){
System.out.println("姓名:" + name + ",年龄:" + age);
}
}
public class Demo2 {
public static void main(String[] args) {
Person p = new Person();
p.name = "小明";
p.age = -18;
p.printInfo();
}
}
输出
姓名:小明,年龄:-18
在上述代码中,我们将年龄赋值为了一个负数,语法上不会有任何问题,程序也可以编译执行,正常输出。但是在现实生活中明显不合理,为了避免这种不合理的问题,在设计java类时,应该对成员变量的访问进行一些限制,不允许外界随意访问,这时就需要类的封装。
4.2 实现封装
类的封装,是指将对象的状态信息隐藏在对象内部,不允许外界直接访问,只能通过该类提供的公共方法来实现对内部信息的操作访问。具体的实现过程是,在定义一个类时,将类中属性
私有化,即使用private关键字修饰,私有属性只能在它所在类中被访问,如果外界需要访问,则提供公共方法来访问这些属性。其中包括设置属性值的setXxx()方法,以及提供公共方法getXxx()方法来获取属性值。
接下来通过案例来演示如何实现类的封装。如例:
class Person {
// 定义私有属性name
private String name;
// 定义私有属性age
private int age =20;
// 定义公共方法getName()来获取name属性
public String getName() {
return name;
}
// 定义公共方法setName()来设置name属性
public void setName(String name) {
this.name = name;
}
// 定义公共方法getAge()来获取age属性
public int getAge() {
return age;
}
// 定义公共方法setAge()来设置age属性
public void setAge(int age) {
if(age <=0){
System.out.println("您输入的年龄不正确!!!");
}else{
this.age = age;
}
}
// 定义公共方法printInfo()来打印对象的信息
public void printInfo() {
System.out.println("姓名:" + name + ",年龄:" + age);
}
}
public class Demo3 {
public static void main(String[] args) {
Person p = new Person();
p.setName("小明");
//年龄传入一个负数
p.setAge(-18);
p.printInfo();
}
}
输出
您输入的年龄不正确!!!
姓名:小明,年龄:20
在上面的例子中,我们使用了private关键字将属性name和age声明为私有变量,并对外界提供了公有的访问方法,getName()用于获取name的值,setName()用于设置name的值,getAge()用于获取age的值,setAge()用于设置age的值,printInfo()用于打印对象的信息。并且在main()方法中创建了Person类对象,并调用了setAge()方法传入了一个负数,但是我们在setAge()方法中进行了年龄的判断,如果年龄小于等于0,则打印提示信息,否则设置年龄。这样,就实现了类的封装,外部代码无法直接访问对象的私有属性,只能通过公共方法来访问和修改对象状态。
5. 方法的重载和递归
5.1 方法的重载
假设在程序中实现一个对数字求和的方法,由于参与求和数字的个数和类型都不确定,因此要针对不同的情况去设计不同的方法。这时,我们可以对方法进行重载,即在同一个类中,可以有多个方法名相同的方法,只要它们的参数个数或参数类型不同即可。
接下来,我们通过一个案例实现两个整数相加、三个整数相加以及两个小数相加的功能,如例:
public class Demo4 {
// 定义一个方法add,用于两个整数相加
public static int add01(int a, int b) {
return a + b;
}
// 定义一个方法add,用于三个整数相加
public static int add02(int a, int b, int c) {
return a + b + c;
}
// 定义一个方法add,用于两个小数相加
public static double add03(double a, double b) {
return a + b;
}
public static void main(String[] args) {
int sum1 = add01(10, 20);
int sum2 = add02(10, 20, 30);
double sum3 = add03(10.5, 20.5);
System.out.println("两个整数相加的和为:" + sum1);
System.out.println("三个整数相加的和为:" + sum2);
System.out.println("两个小数相加的和为:" + sum3);
}
}
输出
两个整数相加的和为:30
三个整数相加的和为:60
两个小数相加的和为:31.0
上述代码不难看出,程序需要针对每一种计算情况都定义一个方法,如果方法名称都不相同,在调用时就很难分清到底应该用哪一个,这时候就需要用到方法重载,定义多个名称相同,但是参数类型或者个数不同的方法,实现方法重载。
接下来按照方法重载的方式,对上述例子就行修改,如例:
public class Demo5 {
// 定义一个方法add,用于两个整数相加
public static int add(int a, int b) {
return a + b;
}
// 定义一个方法add,用于三个整数相加
public static int add(int a, int b, int c) {
return a + b + c;
}
// 定义一个方法add,用于两个小数相加
public static double add(double a, double b) {
return a + b;
}
public static void main(String[] args) {
int sum1 = add(10, 20);
int sum2 = add(10, 20, 30);
double sum3 = add(10.5, 20.5);
System.out.println("两个整数相加的和为:" + sum1);
System.out.println("三个整数相加的和为:" + sum2);
System.out.println("两个小数相加的和为:" + sum3);
}
}
输出
两个整数相加的和为:30
三个整数相加的和为:60
两个小数相加的和为:31.0
虽然两个例子的运行结果一样,但是上述代码中定义了三个同名的add()方法,它们的参数个数或参数类型不同,从而实现了方法的重载。在main()方法中调用add()方法时,通过传入不同的参数便可以确定调用哪个重载的方法。
tip:需要注意的是,方法重载与返回值类型无关,只关注两个条件,一是方法名相同,二是参数个数或参数类型不同。
5.2 方法的递归
方法的递归是指在一个方法的内部调用自身的过程。递归必须要有结束条件,不然就进入了无限递归的状态,永远无法解除调用。
接下来通过一个案例来学习如何使用递归来计算自然数之和,如例:
public class Demo6 {
public static int cal(int n){
if(n == 1){
//满足最后一个数的条件,直接返回1
return 1;
}
//还没到1,进行递归
return n+cal(n-1);
}
public static void main(String[] args){
int num = 10;
System.out.println("1~"+num + "的和为:" + cal(num));
}
}
输出
1~10的和为:55
在上述代码中,cal()方法是一个递归方法,它计算1到n的和,其中n为参数。在cal()方法中,首先判断n是否等于1,如果是,则直接返回1,然后所有递归调用的方法都会以相反的顺序相继结束,所有的返回值进行累加,最终得到结果。否则,调用自身,传入参数n-1,并将结果加上n,进行递归。
6. 构造方法
从前面所学到的知识可以发现,实例化一个类的对象后,如果要为这个对象中的成员进行赋值,则必须直接访问对象的属性或者调用
setXxx()方法。如果需要在实例化对象的同时对这个对象的属性进行赋值,可以通过构造方法来实现。构造方法是类的一个特殊成员,它会在类实例化对象时被自动调用。
6.1 定义构造方法
构造方法的语法格式与定义成员方法的格式相似,语法格式如下:
[修饰符] 类名([参数列表]){
// 构造方法体
}
上述语法格式所定义的构造方法需同时满足以下三个条件:
- 构造方法的名称与类名相同。
- 构造方法没有返回值,也不能有返回值类型。
- 构造方法中不能使用return语句返回一个值,但是可以单独使用return来结束构造方法。
接下来通过案例来演示如何在类中定义构造方法,如例:
class Person {
// 定义私有属性name
private String name;
// 定义私有属性age
private int age;
public Person() {
// 构造方法体
System.out.println("调用无参构造方法");
}
// 定义有参构造方法,用于实例化对象时对name和age进行赋值
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("调用有参构造方法,姓名:" + name + ",年龄:" + age);
}
}
public class Demo7 {
public static void main(String[] args) {
Person p = new Person("小明", 20);
Person p2 = new Person();
}
}
输出
调用有参构造方法,姓名:小明,年龄:20
调用无参构造方法
在上述例子中,定义了一个无参的构造方法Person(),它在类实例化对象时被自动调用,并打印出一条提示信息。同时,定义了一个有参的构造方法Person(String name, int age),它在类实例化对象时被自动调用,并对name和age进行赋值,并打印出姓名和年龄信息。
6.2 构造方法的重载
与普通方法一样,构造方法也可以进行重载。
通过案例,我们来演示构造方法的重载,如例:
class Person {
// 定义私有属性name
private String name;
// 定义私有属性age
private int age;
public Person(int a) {
age = a;
System.out.println("调用有参构造方法,年龄:" + age);
}
public Person(String n,int a) {
name = n;
age =a;
System.out.println("调用有参构造方法,姓名:" + name+",年龄:" + age);
}
}
public class Demo8 {
public static void main(String[] args) {
Person p = new Person(15);
Person p2 = new Person("小明", 20);
}
}
输出
调用有参构造方法,年龄:15
调用有参构造方法,姓名:小明,年龄:20
在上述例子中,定义了两个构造方法,它们的参数个数或参数类型不同,从而实现了构造方法的重载。在main()方法中,分别实例化了两个Person对象,并传入不同的参数,从而调用不同的构造方法。
tip:java的每个类都至少有一个构造方法,如果没有defined构造方法,则系统会自动提供一个默认的构造方法,该构造方法没有参数,也没有返回值。如果我们定义了有参的构造方法,并没有定义无参方法,这个时候编译器会报错。因为定义了有参方法后,必须显示的定义一个无参方法。
7. this关键字
在
构造方法的章节中,我们构造方法使用的参数是a,成员变量使用的是age,语法上没有任何问题,但这样的程序可读性很差,这个时候可以将Person类中表示年龄的变量统一命名为age,但是这样做又会导致成员变量和局部变量冲突,在方法中无法访问成员变量,这时就可以使用this关键字来访问成员变量。
为了解决这个问题,Java中提供了一个关键字this来代表当前对象。this关键字可以用来引用当前对象的实例,可以用于访问当前对象的属性和方法。
接下来会详细说明this关键字在程序中的三种常见用法:
- 通过
this关键字调用成员变量,解决与局部变量同名冲突的问题。
class Person {
// 定义私有属性name
private String name;
public Person(String name) {
//Person类中成员变量为name,局部变量为name,这个时候我们通过this.name来访问的则是 成员变量,以此来解决冲突
this.name = name;
}
}
- 通过
this关键字调用成员方法。
class Person {
public void openMouth(){
//todo
}
public void speak(){
this.openMouth();
}
}
- 通过
this关键字调用构造方法。构造方法是在实例化对象时被Java虚拟机自动调用的,在程序中不能像调用其他方法一样去调用构造方法,但可以在一个构造方法中使用this([参数列表])的形式来调用其他构造方法。
class Person {
// 定义私有属性name
private String name;
// 定义私有属性age
private int age;
public Person() {
System.out.println("调用无参构造方法");
}
public Person(int age) {
this(); //调用无参构造方法
System.out.println("调用了有参构造方法");
}
public Person(String name, int age) {
this(age); //调用有参数构造方法
this.name = name;
System.out.println("我对年龄和姓名进行了赋值,姓名:" + name + ",年龄:" + age);
}
}
public class Demo9 {
public static void main(String[] args) {
Person p = new Person("小明", 20);
}
}
输出
调用无参构造方法
调用了有参构造方法
我对年龄和姓名进行了赋值,姓名:小明,年龄:20
tip:注意,使用this调用类的构造方法时,只能在构造方法中使用this调用其他的构造方法,不能在成员变量中使用;使用this调用的构造方法,必须是该方法的第一条语句,且只能出现一次。
8. static关键字
8.1 静态变量
在定义一个类时,只是在描述某类事物的特征和行为,并没有产生具体的数据。只有通过
new关键字创建该类的实例对象后,系统才会为每个对象分配内存空间,存储各自的数据。有时候,开发人员会希望特定的数组在内存中只存在一份,而且能够被一个类的所有对象所共享。例如某个学校的所有学生都共享一个学校名称,此时完全不必在每个学生对象所占用的内存空间中都声明一个变量存储学校名称,而可以在对象以外的空间声明一个表示学校名称的变量来让所有对象共享。
静态变量是指在类中定义的变量,它属于整个类,不属于任何一个对象,可以被所有对象共享。静态变量可以被类名直接访问,也可以通过对象名来访问。
想要定义静态变量,只需要使用static关键字修饰变量即可,如例:
class Person {
// 定义静态变量schoolName
static String schoolName = "北京大学";
}
public class Demo10 {
public static void main(String[] args) {
Person p1 = new Person();
//通过对象名来访问静态变量
System.out.println("我是"+p1.schoolName+"的学生");
//通过类名来访问静态变量
System.out.println("我是"+Person.schoolName+"的学生");
}
}
输出
我是北京大学的学生
我是北京大学的学生
在上述例子中,定义了一个Person类,其中定义了一个静态变量schoolName,并通过对象名和类名来访问该变量。
tip:static关键字只能用于修饰成员变量,不能用于修饰局部变量。这样是非法的。
8.2 静态方法
静态方法是指在类中定义的
普通方法,它不依赖于任何实例变量,可以被所有对象共享。静态方法可以被类名直接访问,也可以通过对象名来访问。
想要定义静态方法,只需要使用static关键字修饰方法即可,如例:
class Person {
// 定义静态方法
static void sayHello() {
System.out.println("Hello, world!");
}
}
public class Demo11 {
public static void main(String[] args) {
Person p1 = new Person();
//通过对象名来访问静态方法
p1.sayHello();
//通过类名来访问静态方法
Person.sayHello();
}
}
输出
Hello, world!
Hello, world!
8.3 静态代码块
静态代码块是指在类中定义的
代码块,它在类被加载时执行,且只执行一次。静态代码块可以被类名直接访问,也可以通过对象名来访问。
想要定义静态代码块,只需要在类中定义一个static代码块即可。当类被加载时,静态代码块会执行,由于类只加载一次,因此静态代码块也只会执行一次。在程序中通常会使用静态代码块来进行一些类的成员变量初始化操作。如例:
class Person {
// 定义静态代码块
static {
System.out.println("Person类中的静态代码块执行了");
}
}
public class Demo12 {
static {
System.out.println("测试类中的静态代码块执行了");
}
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person();
}
}
输出
测试类中的静态代码块执行了
Person类中的静态代码块执行了
在上述例子中,可以看出程序中的两端静态代码块都执行了。Java虚拟机首先会加载类Demo12。在加载类的同时就会执行该类的静态代码块,紧接着调用main()方法。在main()方法中创建了两个Person对象,但是在两次实例化对象的过程中,静态代码块的内容只输出了一次,这就说明静态代码块只会在类的第一次实际时被加载,并且只会加载一次。