文章目录
JAVA基础知识(四)面向对象基础
把具有共同特征的事物抽象出来,称为对象。比如,把你和身边的同学的某方面的共同特征提取出来,可以得到一个对象——学生。当然不限于身份或职业,家里的电视、甚至是班级等等概念,都可以由具体到普遍再到抽象。编程开发过程中,开发时面对抽象;具体运行时,则对具体进行处理。不过,不需要刻意明白什么是对象,在往后学习应用过程中,能有个模糊的感觉就可以了。
学习过C++等面向对象的语言的话,Java里面向对象的概念都是一致的,具体细节会有所不同。
1. 类
class
,简单理解,就是一个对象。一个.java
文件,必然有个和文件名同名的类。一个类具可以具有属性(也可以叫成员变量或成员常量)和方法。属性是对象的性质,方法是对象的行为。
定义一个类:
<修饰符> class <类名> {
<权限修饰符> <属性1数据类型> <属性1名称> [= <值>]
}
举例:一个典型的web登录功能的用户类
public class User {
// 用户类的属性
private int uid; // 用户ID
private String username; // 用户名
private String password; // 用户密码
private String headimgurl; // 用户头像地址
public User() {// 空构造函数
super();
// TODO Auto-generated constructor stub
}
// 非空构造函数
public User(int uid, String username, String password,String headimgurl) {
super();
this.uid = uid;
this.username = username;
this.password = password;
this.headimgurl = headimgurl;
}
// 对类属性的get和set方法
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getHeadimgurl() {
return headimgurl;
}
public void setHeadimgurl(String headimgurl) {
this.headimgurl = headimgurl;
}
// 对父类toString()方法的重载
@Override
public String toString() {
return "User{" +
"uid=" + uid +
", username='" + username + '\'' +
", password='" + password + '\'' +
", headimgurl='" + headimgurl + '\'' +
'}';
}
}
2. 访问权限修饰符
*表示可以访问
访问修饰符 | 外部包 | 子类 | 本包 | 类内部 |
---|---|---|---|---|
public | * | * | * | * |
protected | * | * | * | |
friendly(default) | * | * | ||
private | * |
-
private
表示私有,只有自己类能访问 -
default
表示没有修饰符修饰,只有同一个包的类能访问 -
protected
表示可以被同一个包的类以及其他包中的子类访问 -
public
表示可以被该项目的所有包中的所有类访问
这里作为了解,明白类、包等概念后再学习亦可。
通常来说,一个类中的属性会被设置为private
,防止外界(简单理解为其他类)直接获取,保证安全。如果外界需要,可以通过暴漏一个public
的方法的方式,给外界提供一个这样的接口,用以获取或修改。
3. static
修饰符
static
称为静态修饰符。用static
修饰的变量、方法,在类加载时就会一并加载,既类存在则类中的该变量(或方法)既存在。因此,主方法(main
函数)是static
的。
用途:在无需创建对象的情况下,通过类本身就可以访问到该类的属性或方法。
调用静态属性:类名.属性名
,调用静态方法:类名.方法名(...)
静态方法内部只能调用静态方法,典型的java程序的主函数既是static
的。
public class DataType {
public void show() {
System.out.println("这是一个非静态方法, 需要通过对象才能调用");
}
public static void showMessage(String msg) {
System.out.println(msg); // 打印参数msg
}
public static void main(String[] args) {
showMessage("这是一个静态方法,在静态方法内部可以直接调用");
// show(); // Non-static method 'show()' cannot be referenced from a static context
// 非静态方法,先创建对象,后调用
DataType d = new DataType();
d.show();
}
}
3.1 静态代码块
属于类调用的代码块,随类的加载而执行一次,用以给类(比如静态变量)进行初始化,其调用优先于
main
函数。
基本格式:
class Person {
// 静态代码块
static {
// 代码
}
}
一个类中可以有多个静态代码块。
4. new
一个对象
通过new
关键字,可以将一个类创建为一个对象,然后就可以通过对象,使用类的public
的非静态方法、访问public
的非静态属性。
基本方式:
<类名> <对象名> = new <类名>(<构造参数>)
举例:
public class DataType {
public static void main(String[] args) {
GirlFriend gf = new GirlFriend();
gf.setName("ivy");
gf.setAge(1);
System.out.println(gf.getInfo());// 我有一个女朋友,她的名字叫做ivy,今年1岁了
}
}
class GirlFriend {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getInfo() {
return "我有一个女朋友," +
"她的名字叫做" + name + ',' +
"今年" + age +
"岁了";
}
}
5. 方法
简单理解就是在一个类(calss
)里面的函数。
一个方法既一个功能,方法内的代码一般不会太长,以实现该功能为目的,过长的代码必然可以从中抽象成另一个方法。
方法的格式:
<修饰符> <返回值类型> <方法名> ([<参数1数据类型> <参数1名称>, ...]) {
// 语句
[return <返回值>]
}
修饰符有访问权限修饰符(public, private, protected
)、静态修饰符(static
)……
无返回值时,返回值类型:void
定义方法时的参数称为形式参数,简称形参。
举例:
public static void show() {
System.out.println("show time...");
}
public static int add(int a, int b) {
return (a + b)
}
5.1 this
在方法内部,可以使用一个隐含的变量
this
,它始终指向当前实例。
class Person {
private String name;
public String getName() {
return name; // 相当于this.name
}
}
// 来源:https://www.liaoxuefeng.com/wiki/1252599548343744/1260452774408320
5.2 方法的调用
在外界:
-
静态方法:
类名.方法名(实参)
-
非静态方法:
对象名.方法名(实参)
PS:调用方法时的参数称为实际参数,简称实参。
在该类内部:
- 直接调用:
方法名(实参)
调用方法时,必须严格按照参数的定义一对一传递,但这种传递本质上是将实参复制了一份再赋值到形参,因此对于基本数据类型会发生自动类型转换。
方法有返回值时,可以将调用语句当作一个,变量类型为方法返回值类型的“变量”来使用。如上诉例子中的System.out.println(gf.getInfo());
,将gf.getInfo()
看作String
类型的变量,便可直接在System.out.println()
中使用。
举例:
public class DataType {
public static void main(String[] args) {
// 我想发送封邮件,但是不需要签名,直接调用静态方法
EmailUtils.sendEmail("你好,世界");
// 我想发送一份签名的邮件
EmailUtils eu = new EmailUtils();
eu.setEmailName("Heat");// 设置签名
eu.sendEmailAfterSignature("I'm Heat but feel so cold...Can you help me?");// 发送邮件,调用非静态方法需要先创建对象(new)
}
}
/**
* EmailUtils: 帮助我发送邮件的工具类
*/
class EmailUtils {
private String emailName;//邮件签名
public String getEmailName() {
return emailName;
}
public void setEmailName(String emailName) {
this.emailName = emailName;
}
/**
* 签名方法,private
* @param emailContent 邮件内容
* @return 签名后邮件内容
*/
private String signature(String emailContent) {
return emailContent + "——【" + emailName + "】";
}
/**
* 发送邮件,不签名
* @param emailContent 邮件内容
*/
public static void sendEmail(String emailContent) {
System.out.println("发送了邮件.....");
System.out.println("邮件内容:");
System.out.println(emailContent);
System.out.println("---------------------------");
}
/**
* 签名后发送邮件
* @param emailContent 邮件内容
*/
public void sendEmailAfterSignature(String emailContent) {
System.out.println("签名中......");
// 内部可以直接调用private方法
String rs = signature(emailContent);
System.out.println("邮件内容:");
System.out.println(rs);
System.out.println("---------------------------");
}
}
/*运行结果:
发送了邮件.....
邮件内容:
你好,世界
---------------------------
签名中......
邮件内容:
I'm Heat but feel so cold...Can you help me?——【Heat】
---------------------------
*/
5.2.1 递归
方法在语句块中自己调用了自己。
举例:递归计算1加到100
public class DataType {
public static int getSum(int num) {
if (num == 1) return 1;
return num + getSum(num - 1);
}
public static void main(String[] args) {
System.out.println(getSum(100));
}
}
// 运行结果:
// 5050
一个递归函数(方法),有必须的两部分:
- 递归结束条件:决定递归何条件下结束
- 递归:决定怎么样进行递归,主要体现在参数的变化。
PS:递归次数过多会引起栈内存溢出。
递归是很有意思的语法现象,它不符合人类思考的习惯,但在计算机中合理的运用却可以方便的实现一些算法,典型的就是:深度搜索算法、广度搜索算法。
5.3 方法的重载
Overload
,如果几个方法的方法名相同(通常返回值也相同),但各自的参数不同,称为方法的重载。
参数不同:
- 参数个数不同
- 参数数据类型不同
- 数据类型顺序不同(一般也没人这么写重载…但是我得告诉你可以这么做。。)
举个例子,
String
类提供了多个重载方法indexOf()
,可以查找子串:
int indexOf(int ch)
:根据字符的Unicode码查找;int indexOf(String str)
:根据字符串查找;int indexOf(int ch, int fromIndex)
:根据字符查找,但指定起始位置;int indexOf(String str, int fromIndex)
根据字符串查找,但指定起始位置。来源:方法重载
5.4 构造方法
用来构造对象时调用的方法,构造方法方法名和类名一致,权限修饰符为
public
构造方法在我们new
一个对象时会自动调用,有时我们不需要传参给类属性初始化,有时我们需要传参给类属性初始化。
因此,每个类都有一个默认的构造方法,不需要我们手动编写,便存在。
public class DataType {
public static void main(String[] args) {
// 调用默认的构造函数
User u = new User();
}
}
class User {
private String name;
// 默认存在,不需要我们写,也不会现实存在在代码中,这里是为了说明其格式。
public User(){}
}
如果我们需要,可以写上自己需要的构造方法。
public class DataType {
public static void main(String[] args) {
// 一旦我们自己写了,默认的就消失掉了,就不能这么调用了
//User u = new User();
// 现在只能这么写啦
User u = new User("Heat");
}
}
class User {
private String name;
public User(String name) {
this.name = name;
}
}
如果还需要,那就必须自己写上无参构造就好了,就像第一个例子那样。
构造函数可以重载,针对不同的需要,编写不同的构造函数。
6. 包
package
,java中的一种名字空间,可以通俗的理解成文件夹。一个类总是属于一个包,所以一个类的完整名字是包名.类名
。
申明一个类(这里指和.java
文件同名的那个类)属于某个包用package
,通常放在.java
文件内容最上一行。没有申明时,则使用的是默认包。
包名的命名规范:
-
一个单词:小写英文单词
-
多个单词:公司域名的倒写,且单词间用
.
符号间隔。实际上,若包名为run.heat.java
则可以看作有如下文件夹结构run | ┕---heat | ┕---java | t1.java | t2.java
例如:
package run.heat.java;
public class Study {
public static void main(String[] args) {
System.out.println("Hello,Heat");
}
}
7. 继承
当对象需要由抽象逐渐变得具体时,就需要通过继承抽象的上一层的通用属性或方法,再新生出独有的属性或方法。
继承,既从父类直接继承属性或方法,同时也表明了自己和父类的关系。
可以举一个具体的例子:
汽车
┎┄┄┄┄┄┄┄┄┄┄┄┄┇┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┓
| | |
电动汽车 油电混合汽车 燃油汽车
汽车类具有一些属性,如:车轮、发动机、车窗……,而其子类(电动汽车、燃油汽车等)通过继承可以直接获得这些属性,子类不需要再重复编写。这样一来,父类与子类不仅在逻辑上成立,从代码上看也成立。
继承的定义:extends
关键字
// 存在的父类
class Car {
// Car类的属性及方法
}
// 定义的子类
class FuelCar extends Car {
//FuelCar类的属性及方法,已继承Car的属性及方法
}
所有没有明确写extends
关键字的类均继承自Object
类。可以说,Object
类是最上一层的类(当然,你也可以叫它:祖先、亚当、夏娃……)。
一个类只允许继承自一个父类。Java舍去了多继承方式,也是因为对多继承这种多害少利的语法进行了取舍。“在Thinking in Java里面,作者已经指出——如果确定在不需要多态(把子类转为父类)的情况下,应该优先考虑组合(将该类的实例作为另一类的属性,从而复用代码)而不是继承。”
子类不能直接访问父类private
修饰的属性或方法。
子类访问父类的属性:[super.]<父类属性>
,例如:super.name
子类访问父类的方法:[super.]<父类方法>()
,例如super.getName()
super
可以省略的情况是:属性或方法不存在歧义,既子类没有和父类重名的属性或方法。
在子类的构造方法中,第一条语句必须是调用父类的构造方法:super(<参数>)
,如果没有明确的调用,编译器会自动帮我们加一行super()
,此时必须保证父类存在无参构造。
例子:
public class HelloWorld {
public static void main(String[] args) {
FuelCar fc = new FuelCar("特斯拉", "特斯拉", "Goodbaby", "特斯拉");
System.out.println(fc.toString());
}
}
// 父类
class Car {
protected String engine;// 引擎
protected String tire;//轮胎
protected String seat;//座椅
public Car() {
}
public Car(String engine, String tire, String seat) {
this.engine = engine;
this.tire = tire;
this.seat = seat;
}
public String getEngine() {
return engine;
}
public void setEngine(String engine) {
this.engine = engine;
}
public String getTire() {
return tire;
}
public void setTire(String tire) {
this.tire = tire;
}
public String getSeat() {
return seat;
}
public void setSeat(String seat) {
this.seat = seat;
}
}
// 子类
class FuelCar extends Car {
protected String powerRecoverySystem; // 电源回收系统
public FuelCar() {
}
public FuelCar(String engine, String tire, String seat, String powerRecoverySystem) {
super(engine, tire, seat);
this.powerRecoverySystem = powerRecoverySystem;
}
// 重写toString()方法。
@Override
public String toString() {
return "FuelCar{" +
"engine='" + engine + '\'' +
", tire='" + tire + '\'' +
", seat='" + seat + '\'' +
", powerRecoverySystem='" + powerRecoverySystem + '\'' +
'}';
}
}
7.1 向上转型
定义一个父类变量指向子类实例,既称这是子类的向上转型
例如:
Car c = new FuelCar();
将子类安全的变为更加抽象的父类。这样,面向父类设计的方法(形参为父类),传入一个子类依然适用,符合逻辑直觉。
7.2 instanceof
关键字
instanceof
是用来对实例类型做判断(布尔运算)的关键字,若该实例是类的实例或子类实例,则结果为true
,否则为false
。
基本格式:
<实例> instanceof <类名>
举例:
FuelCar fc = new FuelCar();
System.out.println(fc instanceof FuelCar);//true
// 与父类做判断
System.out.println(fc instanceof Car);//true
// 与Object做判断
System.out.println(fc instanceof Object);//true
7.3 方法的覆写
Override
,覆写父类的继承过来的方法。
IDea
中使用快捷键ctrl + O
即可快捷的查看可重写的方法。
一个典型的例子就是重写Object
类中的toString()
方法,见第7节
中的例子。
加上@Override
可以使得Idea
自动进行覆写检查。
8. 多态
当子类覆写了父类的方法时,由于存在向上转型等机制,所以调用该方法时则会根据调用该方法的实例的类型选择调用父类或子类的方法。
多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。
利用多态,编写代码时可以针对父类编程,在实际运行时则可以自由传入不同的子类的实例,以实现同一套代码可以重复利用。
“多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。”
例子:
public class HelloWorld {
public static void main(String[] args) {
Cat cat = new Cat("英短", 0, "黑色");
animalCalled(cat);
System.out.println("---------------------------------");
Dog dog = new Dog("哈士奇", 0, "白色");
animalCalled(dog);
}
// 针对父类编程
public static void animalCalled(Animal animal) {
animal.called();// 调用动物叫的方法
}
}
class Animal {
protected String type; //动物类型
protected int sex; // 动物性别,0:公,1:母
protected String color; // 动物颜色
public Animal(String type, int sex, String color) {
this.type = type;
this.sex = sex;
this.color = color;
}
public void called() {
System.out.println("这只动物叫了...");
}
}
class Cat extends Animal {
public Cat(String type, int sex, String color) {
super(type, sex, color);
}
@Override
public void called() {
System.out.println("喵喵喵~~~~~~");
}
}
class Dog extends Animal {
public Dog(String type, int sex, String color) {
super(type, sex, color);
}
@Override
public void called() {
System.out.println("汪汪汪~~~~~~");
}
}
// 运行结果:
// 喵喵喵~~~~~~
// ---------------------------------
// 汪汪汪~~~~~~
9. 抽象类
abstract
,存在抽象方法的类,必须被声明为抽象类,且抽象类将无法被实例化。抽象方法:某个方法在父类不需要写任何代码(或实际并不执行父类的该方法),实际代码均交由子类覆写时,可以用
abstract
关键词修饰该方法,此时,这个方法被称抽象方法。
基本格式:
abstract class <父类类名> {
//抽象类可以定义属性,和普通类无异
//抽象方法,不具有方法体(”{}“)。
abstract <修饰符> <返回值类型> <方法名>(<形参类型> <形参>);
//抽象类依旧可以定义普通方法
}
抽象类的子类则必须覆写抽象方法。
举例:将第9节
的代码稍作修改
public class HelloWorld {
public static void main(String[] args) {
Cat cat = new Cat("英短", 0, "黑色");
animalCalled(cat);
System.out.println("---------------------------------");
Dog dog = new Dog("哈士奇", 0, "白色");
animalCalled(dog);
}
public static void animalCalled(Animal animal) {
animal.called();
}
}
abstract class Animal {
protected String type;
protected int sex;
protected String color;
public Animal(String type, int sex, String color) {
this.type = type;
this.sex = sex;
this.color = color;
}
abstract public void called();
}
class Cat extends Animal {
public Cat(String type, int sex, String color) {
super(type, sex, color);
}
@Override
public void called() {
System.out.println("喵喵喵~~~~~~");
}
}
class Dog extends Animal {
public Dog(String type, int sex, String color) {
super(type, sex, color);
}
@Override
public void called() {
System.out.println("汪汪汪~~~~~~");
}
}
10. 接口
interface
,剔除掉抽象类中非抽象部分(属性及非抽象方法),剩下的则高度抽象,此时这样的一个抽象类既等同Java中接口的概念。
基本格式:使用interface
关键字
interface <接口名> {
//定义接口中的方法
<返回值类型> <方法名>(<形参类型> <形参名>);
}
举例:
interface Animal {
void called();
}
10.1 实现接口
使用关键字implements
基本格式:
class <类名> implements <接口名1>[,<接口名2>] {
// 需要覆写的方法
}
和继承不同,一个类可以实现多个接口。
综合例子:在第9节
的基础上稍作修改
public class HelloWorld {
public static void main(String[] args) {
Cat cat = new Cat();
animalCalled(cat);
System.out.println("---------------------------------");
Dog dog = new Dog();
animalCalled(dog);
}
public static void animalCalled(Animal animal) {
animal.called();
}
}
interface Animal {
void called();
}
class Cat implements Animal{
public void called() {
System.out.println("喵喵喵~~~~~~");
}
}
class Dog implements Animal{
public void called() {
System.out.println("汪汪汪~~~~~~");
}
}
可以看到,如果无需在父类定义属性值等,面向接口的代码设计会更加简洁。并且由于可以实现多个接口的特性,接口实际也就成了一种“方法规范”。
11. import
导入类
导入其他包中的类供当前类使用其方法或属性。
基本格式:
import <完整类名>;
完整类名是指将类所在包也描述出来的完整类名称,如java.util.ArrayList
举例:
// 导入ArrayList类
import java.util.ArrayList;
// 导入List接口
import java.util.List;
public class HelloWorld {
public static void main(String[] args) {
// 面向接口的写法,list可以认为是一个数组。
List<Integer> list = new ArrayList<>();
// 对数组加入元素
for (int i = 1; i <=10 ; i++) {
list.add(i);
}
// 使用到了List的for-Each方法,传入了一个Lambda表达式,用来遍历并打印list数组中的元素
list.forEach(item -> {
System.out.print(item + " ");
});
}
}