java-面向对象

Last updated on a day ago

面向对象的概念

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候依次调用。面向对象则是把构成问题的事务按照一定规则划分为多个独立对象,然后通过调用对象的方法来解决问题,通过多个对象配合来解决问题,这样当程序功能需要改动时,只需要修改个别的对象就可以了,从而更容易维护。

面向对象的特点

主要可以概括为:封装性继承性多态性

  • 封装性:封装是面向对象的核心思想,将对象的属性和行为封装起来,不需要让外界知道具体实现环节,这就是封装思想。例如:用户使用计算机只需要手指敲键盘就行了,不需要了解计算机的工作原理,就算知道,在使用时也并不依赖这些原理。
  • 继承性:继承性主要描述的时类与类之间的关系,通过继承,可以在无需重新编写原有类的情况下,对原有类的功能进行扩展。例如有个汽车的类,描述了汽车的普遍特征和功能,而轿车的类中不仅要包含汽车的特征和功能,还应该添加轿车特有的功能。这时可以让轿车类继承汽车类,然后单独添加轿车特征。继承不仅增强了代码的复用性,提高了开发效率,还为程序的维护提供了便捷。
  • 多态性:多态性指的是在程序中允许出现重名的情况,它指在一个类中定义的属性和方法被其他类继承后,它们可以具有不同的数据类型或表现出不同的行为,这使得同一个属性和方法在不同的类中有不同的含义。

类与对象

类是对某一类事物的抽象描述,而对象用于表示现实中该类事物的个体。比如玩具模型可以看成一个类,将一个个玩具看成对象。

类用于描述多个对象的共同特征,它是对象的模板。对象用于描述现实中的个体,它是类的实例。

类的定义

要在程序中创建对象,首先需要定义一个类,类是对象的抽象,用于描述一组对象的共同特征和行为。类中可以定义成员变量和成员方法,其中成员变量用于描述对象的特征,也被称为属性;成员方法用于描述对象的行为,可简称为方法

1
2
3
4
5
6
7
8
9
//定义Person类
class Person{
int age; //定义int类型的成员变量age

//定义speak()方法
void speak(){
System.out.println("大家好,我今年" + age + "岁");
}
}

在Java中,定义在类中的变量被称为成员变量,定义在方法中的变量称为局部变量,如果在某个方法中定义的局部变量与成员变量同名,是允许的,此时方法中通过变量访问到的是局部变量,而非成员变量

对象的创建与使用

在程序中,可以使用new关键字创造对象

1
2
3
4
类名 对象名称 = new 类名();

//创建Person类的实例对象
Person p = new Person();

在上述代码中,“new Person()”用于创建Person类的一个实例对象,”Person p”则是定义了一个Person类型的变量p。中间的等于号将Person对象在内存中的地址赋值给变量p,这样p就持有了对象的引用。

在创建对象后,可以通过对象的引用来访问对象的所有成员

1
2
3
4
5
6
对象引用.对象成员

//访问Person p中的age成员和speak()方法
Person p = new Person();
p.age = 10;
p.speak();

运行的结果为”我的年龄是10岁”

在实例化对象时,针对成员变量的类型的不同,java虚拟机将会赋予不同的初值。

成员变量类型初始值
byte0
short0
int0
long0L
float0.0F
double0.0D
char空字符'/u0000'
booleanfalse
引用数据类型null

当对象被实例化后,在程序中可以通过对象的引用变量来访问该对象的成员。需要注意的是,当没有任何变量引用这个歌对象时,它将成为垃圾对象,不能再被使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
void say(){
System.out.println("我是人");
}
}

class test{
public static void main(String[] args){
Person p1 = new Person();//创建Person对象
p1.say();
p1 = null;
p1.say();
}
}

运行结果:
我是一个人
报错信息

在将p1赋值null后,p1指向了一个空指针,被p1引用的Person对象就会失去引用,成为垃圾对象

类的封装

类的封装是指在定义一个类时,将类中的属性私有化,即使用private关键字来修饰私有属性,使他只能在它所在的类中被访问,如果外界想要访问私有属性,需要提供一些使用public修饰的公有方法,其中包括用于获取属性值的getXxx方法和设置属性值的setXxx方法。

构造方法

在实例化一个对象后,要修改对象的属性必须要通过直接访问对象的属性或setXxx方法的方式才可以。如果要在实例化的同时也为属性赋值,则需要通过构造方法。构造方法是类的一个特殊成员,在类实例化时自动调用。

构造方法的定义

首先构造方法必须满足三个要求

  1. 方法名与类名相同
  2. 方法名的前面没有返回值类型的声明
  3. 在方法中不能使用return语句返回一个值,但可以单纯些return语句作为方法的结束

无参构造方法实例

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person{
//构造方法
public Person(){
System.out.println("无参的构造方法被调用了");
}
}

public class Test{
public static void main(String[] args){

Person p1 = new Person();//实例化对象
}
}

运行结果:
无参的构造方法被调用了

既然有无参的,那必有有参

有参构造方法实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
int age;//定义age属性

//定义有参的构造方法
public Person(int a){
age = a;
}

public void say(){
System.out.println("今年我" + age + "岁了");
}
}

public class Test{
public static void main(String[] args){
Person p1 = new Person(10);//实例化对象,同时输入参数
p1.say();
}
}

运行结果:
今年我10岁了

构造方法的重载

和普通方法一样,构造方法也可以重载,重载就是允许多个同名方法存在,只要参数个数或参数类型不同即可。


每个类都至少有一种构造方法,若没定义,则系统会自动创建一个默认的构造方法,也就是说,以下两种方式是等效的

1
2
3
4
5
6
7
//不定义构造方法
class Person{}

//定义无参无内容的构造方法
class Person{
public Person(){}
}

这也就是为什么,我们未定义任何构造方法却仍可以使用new Person()实例化对象

若定义了构造方法,则系统会舍弃默认构造方法,如定义了一个有参的Person(int a),则在实例化对象时无法使用new Person(),

所以在定义构造方法时,为了防止出错,最好在手动定义一个默认方法


this关键字

像我们刚刚定义有参构造方法时,将a赋值给age属性,这样的定义方法会影响程序的可读性,最好是将表示年龄的变量统一为age,可这样会导致局部变量和全局变量冲突,使得方法无法访问全局成员变量age,为了解决,java提供了this关键字指代当前对象

1
2
3
4
5
6
7
8
//this调用成员
this.age;

//this调用方法
this.say();

//this调用构造方法
this(参数123...);

对于第三顶,需要注意以下几点

  • 只能在构造方法中使用this调用其他构造方法,不能再成员方法中使用
  • 在构造方法中,使用this调用构造方法的语句必须位于第一行,且只能出现一次
  • 不能在一个类的两个构造方法中使用this互相调用

垃圾回收

​ 在java中,一个对象成为垃圾后仍会占用内存,时间一长导致内存不足。于是java引入了垃圾回收机制,java虚拟机会自动回收垃圾对象所占用得内存。当垃圾堆积到一定程度时就会自动释放,也可以调用

1
System.gc();

方法来通知java虚拟机立即进行垃圾回收。当一个对象在内存中被释放时,它的finalize()方法会被自动调用,因此可以通过定义finalize()方法来观察对象何时被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person{
public void finalize(){
System.out.println("对象作为垃圾被回收");
}

}
public class test{
public static void main(String[] args){
Person p1 = new Person();
Person p2 = new Person();
p1 = null;
p2 = null;
System.gc();

}
}

运行结果:
对象作为垃圾被回收
对象作为垃圾被回收

static 关键字

静态变量

在定义一个类时,只是描述某类事物的特征和行为,并不会产生具体的数据,只有在new创建实例对象后,系统才会为每个对象分配空间,存储各自的数据。但有时我们希望某个特征被所有对象共享,比如学校中的每个学生都在一个学校内,他们的学校名称地址等属性应是相同的。此时不需要在每个学生的内存空间中都定义学校名称,可以在对象以外的空间定义一个表示学校名称的变量,用于共享。

1
2
3
4
5
6
7
8
class Student{
static String schoolName;
}
public class Test{
public static void main(String[] args){
Student.schoolName = "学校名";
}
}

静态方法

在开发时,有时会希望不创建对象就调用某个方法,只需要在定义的方法钱加上static关键字即可。静态方法可以使用类名.方法名调用。

静态代码块

用一对大括号括起来的几行代码被称为代码块,用static修饰的代码块被称为静态代码块。当类被加载时,静态代码块会被自动执行,由于类只加载一次,因此静态代码块也只执行一次。

内部类

在Java中允许在一个类的内部定义类,称为内部类,根据内部类的位置、修饰符和定义的方式可分为成员内部类、静态内部类、匿名内部类、局部内部类。相对的,内部类所在的类称为外部类

成员内部类

成员内部类是普通的内部类,成员内部类可以无限制访问外部类的所有成语和方法(包括private和静态成员),当内部类存在和外部类同名的方法或成员时,默认访问的是内部类中的方法或成员,此时可以用this访问外部类。

虽然成员内部类可以无限制访问外部类,但外部类访问成员内部类的成员就需要先创建一个内部类对象,再通过这个对象来访问。

创建成员内部类对象

成员内部类是依靠外部类存在的,所以要创建成员内部类对象要先创建外部类对象,方法有2种

1
2
3
4
5
6
7
8
9
10
11
12
class Outter{
class Inner{
System.out.println("1");
}
}

//方法一
Outter outter = new Outter();
Outter.Intter Intter = outter.new Inner();

//方法二
Outer.Inner inner = new Outter(). new Inner();

局部内部类

局部内部类和成员内部类类似,但是是定义在方法或作用域内,且没有访问外部类的权限

1
2
3
4
5
6
7
8
9
class Outter {
//定义方法
public void f() {
//方法中定义局部内部类
class Inner() {

}
}
}

匿名内部类

静态内部类

静态内部类相当于外部类的一个类属性,可以脱离外部实例化,直接初始化

创建静态内部类

只需要在成员内部类的前面加public static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Outter {
public Outter() {
System.out.println("外部类被创建");
}
public static class Inner {
public Inner() {
System.out.println("内部类被创建");
}
public int i = 1;
}
}
public class hello{
public static void main(String args[]){

//脱离外部实例化,直接初始化
Outter.Inner inner = new Outter.Inner();
System.out.println(inner.i);
}

}

结果

1
2
内部类被创建
1

继承

继承的概念

类的继承是指用现有的类去创建新的类,子类可以继承父类的属性和方法

继承通过extends关键字声明,格式为:

1
2
3
4
class SonClass extends FatherClass {
//重载或自定义方法、属性

}

比如dog狗这一类,可以从animal动物类继承,同时

1
2
3
class Dog extends Animals {
//重载或自定义方法、属性
}

在java中只有单继承,一个类只能有一个父类,但是可以有多个子类

重写父类方法

在继承中,子类会自动继承父类的方法,但有时需要对父类的方法进行一些修改,这就需要在子类的对父类的方法进行重写(不是重载,重载在前面)

1
2
3
4
5
6
7
8
9
10
11
class FatherClass {
public void say() {
System.out.println("I am father");
}
}
class SonClass extends FatherClass {
// 对父类同名方法进行重写
public void say() {
System.out.println("I am son");
}
}

super关键字访问父类成员

在重写父类方法后,我们直接调用的就是重写后的方法,如果想调用父类未重写的方法,就需要通过**super关键字**

1
2
3
4
5
6
7
8
9
10
class SonClass extends FatherClass {
// 对父类同名方法进行重写
public void say() {
System.out.println("I am son");
}
// 声明新方法通过super调用父类方法或属性
public void fSay() {
super.say();
}
}

super也可以访问父类的构造函数方法

1
2
3
4
5
6
class SonClass extends FatherClass {
// 调用构造函数
public void f() {
super([参数]);
}
}

在初始化子类的时候,父类的构造函数会自动调用,如果父类的构造函数会对成员进行修改,修改后的成员也会被继承到子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FatherClass {
// 声明成员变量
int a = 1;
// 声明无参构造函数
public void FaherClass() {
// 修改成员变量
a = 10;
}
}
class SonClass extends FatherClass {
// 声明构造函数
public void SonClass() {
// 打印继承的成员变量,结果为10
System.out.println(a);
}
}

注意:如果在父类只声明了有参构造函数,则在子类的继承中,必须显式的调用父类的构造方法,否则会报错

1
2
3
4
5
6
7
8
// 继承
class SonClass extends FatherClass {
// 子类声明构造函数
public SonClass() {
// 显式调用父类的构造函数
super([参数]);
}
}

final关键字

final关键字可以用来修饰类、变量和方法

  • 被修饰的类不能被继承
  • 被修饰的方法不能被子类重写
  • 被修饰的变量是常量,只能被赋值一次

抽象类和接口

抽象类

在定义类的时候一般都需要定义一些方法来描述它的行为特征,比如dog类可以有shout()方法,表示它的叫声,但对于比较广泛的类,比如animal类,dog可以从animal继承到shout方法,但是每个动物的叫声都不同,那么animal类中的shout方法该怎么写呢?

可以留空交给子类重写。不写方法体的方法就是抽象方法。带有抽象方法的类就是抽象类。抽象类必须用abstract关键字修饰,用abstract关键字修饰的抽象类不一定含有抽象方法

1
2
3
4
5
6
7
8
9
10
11
// 声明抽象类
abstract class FatherClass {
// 声明抽象方法
abstract void f();
}
class SonClass extends FatherClass {
// 对抽象方法重写
public void f() {
// 方法体内容
}
}

在使用的时候需要注意以下几点:

  • 方法体为空不等于抽象方法:比如void f(){}并不是一个抽象方法,不可以用abstract修饰。抽象方法是没有方法体,而不是方法体为空
  • 抽象方法一定要被重写:在子类继承之后,抽象方法一定要进行重写,*否则会报错
  • 抽象类不能被实例化:如果进行编译,会提示“xx是抽象的; 无法实例化”

接口

接口是由常量和抽象方法组成的特殊类,是对抽象类的进一步抽象。定义接口用**interface**关键字修饰,格式如下:

1
2
3
4
[public] interface 接口名 [extends 接口1,接口2...] {
[public] [static] [final] 数据类型 常量名 = 常量值;
[public] [abstract] 返回值 抽象方法名([参数列表]);
}

接口克服了类单继承的缺点,一个接口可以通过extends继承多个父接口一个类可以实现多个接口(类重写接口的抽象方法称为实现,可以理解成不同类型间的继承)。接口中定义的方法默认是**public abstract如果接口为public则接口中的变量和方法全部为public**。

由于接口里只有抽象方法,无法进行实例化,需要通过类似extends继承的方法去调用并重写抽象方法,这需要用到关键字**implements**(实现)。格式如下:

1
[<修饰符>] class <类名> [extends <父类名>] [implements] <接口1>,<接口2>...]

和抽象类一样,子类需要对父类进行重写,但是不同的是,如果实现接口的是一个抽象类,则可以只实现部分接口

多态

多态的概念

多态也就是一个对象拥有多种形态,具体指的为编译时和运行时的状态不同。代码中体现为父类做类型,子类做实现(父类引用指向子类),代码格式为:

1
2
父类名称 对象名 = new 子类名称();
接口名称 对象名 = new 实现类名称();

举个例子

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
38
// 定义父类
class Fa {
// 定义成员变量
final String a = "123";
public void say() {
System.out.println("Fa");
}
}
// 子类继承
class Son extends Fa {
// 定义同名成员变量
final String a = "345";
// 方法重写
public void say() {
System.out.println("Son");
}
// 定义子类特有的方法
public void say2() {
System.out.println("s2");
}
}

// 测试
public class test {
public void main(String[] args) {
// 多态
Fa son = new Son();

// 编译时看父类,运行时看子类,调用的是子类的say方法
son.say();
// 编译时看父类,父类没有say2方法,编译不通过
son.say2();
// 编译时看父类,son.a调用的是父类的成员变量
System.out.println(son.a)


}
}

对比之下,多态的写法更加统一。

那么多态中会执行哪个类中的方法呢?答案是子类的中的方法,new的是哪个类,使用的就是谁的成员方法,如果没有,就会到父类找。而如果是访问成员变量,则是相反,用的是父类的成员变量,总结就是编译看(等号)左边,运行看右边

多态应用

有时,我们可能会有多个子类,比如Animal,可以有Cat、Dog等子类,假设现在我需要写一个方法,要求输入Animal类的对象,自动使用其中的方法,如果java没有多态性,那么你需要给每一种子类单独写一个方法

1
2
3
4
5
6
7
// 为子类声明方法
public staic void useAnimal(Dog dog) {
dog.shout();
}
public staitc void useAnimal(Cat cat) {
cat.shout();
}

而由于多态性,现在你可以用父类Animal做形参类型,并支持传入子类类型

1
2
3
4
5
6
7
8
9
10
11
// 声明方法
public static void useAnimal(Animal an) {
an.shout();
}

// 实例化子类
Dog dog = new Dog();
Cat cat = new Cat();
// 调用方法
useAnimal(dog);
useAnimal(cat);

上面的方法可以把实例化省略,变成

1
useAnimal(new Dog())

而由于我们形参是Animal。所以在userAnimal方法内部,实际上是

1
Animal an = new Dog();

类型转换

在上面的代码中,我们将Dog类传递给了Animal类,可以理解为进行了类型转换,这种称为”向上转型“,我们在使用useAnimal()方法时,虽然传入了Dog对象,但是并不能使用Dog中特有的方法,只能使用Animal类中的方法,如果想使用,就需要对Animal进行类型转换,变为Dog()子类,这种转换称为”向下转型“。方法如下

1
2
3
4
public static void useAnimal(Animal an) {
Dog dog = (Dog) an;
dog.dogShout();
}

其实就是进行强制类型转换

但是由于有多个子类,我们要怎么判断传入的类是不是Dog呢?就需要使用instanceof关键字

instanceof判断类

可以用来判断对象是否为某个类或接口的实例或子类实例,格式如下:

1
对象(或对象引用变量) instanceof 类(或接口)

它会返回bool值。

Object 类

Object类是所有类的父类或间接父类,所有对象(包括数组)都实现了这个类的方法。Object类中的常用方法有

方法名称 说明
equals() 指示其他某个对象是否与此对象“相等”
getClass() 返回此Object的运行时类
hashCode() 返回该对象的哈希值
toString() 返回该对象的字符串表示

下面是个演示toString的例子

1
2
3
4
5
6
7
8
9
10
11
class A {
public void fa() {
System.out.println("A");
}
}
public class test {
public static void main(String[] args) {
A a = new A();
System.out.println(a.toString());
}
}

对象a中没有定义toString方法但仍能正常使用,这是因为从Obejct中继承了。

toString的代码具体如下

1
getClass().getName() + "@" + Integer.toHexString(hashCode());

匿名内部类

匿名内部类是定义在类的内部,没有名字的内部类,是一种实现接口的简便方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) {
// 定义匿名内部类实现接口并传入
Animalshout(new Animals() {
public void shout() {
System.out.print("喵喵");
}
});
}
// 定义静态方法调用shout
public static void Animalshout(Animals an) {
an.shout();
}
}
// 定义接口
interface Animals {
void shout();
}