文章目录
一、类与对象
什么是对象
对象:是一个一个实实在在的,具体的个体(实体)。比如:一个个人、一辆辆车、一本本书等等。
什么是类
类:一组具有相同特征的对象的抽象描述
如:
张三,510220199612013669,18256958568,158936587… 代表一个对象
李四,510220199612013667,18256958575,158936787… 代表一个对象
姓名、身份证号码、手机号码、qq号码… 对应一个类
此时就可以将这些对象抽取相同的特征(属性)封装为一个类
public class Person {
String name;//名称
String idCard;//身份证
String phone;//手机
String qq;//qq号
}
通俗而言:类就相当于施工图,我们可以根据施工图来知道一栋楼每套房子具有那些功能区(共同的特质),进而造出楼房。
在Java中根据施工图创建对象的关键字是 new。
从代码编写的角度而言,必须先创建类,才能创建出对象。
什么是面向对象
面向对象是一种思想或编程方式。
面向对象的过程是以“对象”为中心。
Scanner input = new Scanner(System.in);//input就是一个Scanner类的对象
System.out.print("请输入姓名:");
String name = input.next();
System.out.print("请输入年龄:");
int age = input.nextInt();
input.close();
与面向对象对应的另一种编程思想:面向过程。它是以函数为中心,实现代码就是通过函数调用把过程串起来。
public class ArrayTools{
public static int max(int[] arr){
int max = arr[0];
for(int i=1; i<arr.length; i++){
if(arr[i] > max){
max = arr[i];
}
}
return max;
}
public static void print(int[] arr){
for(int i=0; i<arr.length; i++){
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args){
int[] arr = {2,4,5,3,1};
print(arr);
int result = max(arr);
System.out.println("最大值:" + result);
}
}
这一段代码,如果抛开ArrayTools类的定义,那么3个函数是独立的,它们互相调用即可,这种方式就是面向过程的编程思想。
另外一个场景描述,来理解面向对象与面向过程的区别:
把大象装进冰箱的问题?
面向过程 | 面向对象 |
---|---|
以步骤为中心,即以函数为中心 | 以类/对象为中心 |
(1)把冰箱门打开 (2)把大象装进去 (3)把冰箱门关上 | (1)定义人这个类,包含pull,push功能,包含name属性 (2)定义冰箱这个类,包含open,close,save功能,包含size、brand等属性 (3)定义大象这个类,包含walk功能,包含weight属性 (4)定义测试类,包含main方法,在main方法中创建人、冰箱、大象的对象,调用对象的方法来完成 |
如何定义类
语法格式:
【①修饰符】 ②class ③类名{
④类的成员
}
如:
public class Person{
String name; //类的成员
}
如何new对象
语法格式:
new 类名()
如果没有用变量引用对象,这个对象是一个“匿名对象”。匿名对象都是一次性,无法反复使用
public class Test {
public static void main(String[] args) {
new Person();//创建了一个Person类的对象
System.out.println(new Person());//创建一个Person类的对象,并且打印这个对象
//com.yang.dto.Person@3b07d329
System.out.println(new Person());//又创建一个Person类的对象,并且打印这个对象
//com.yang.dto.Person@41629346
//上述三个对象是匿名对象,都是独立的,无法重复使用的
}
}
如何用变量引用一个对象?
类名 对象名 = new 类名();
对象名本质上也是一个变量,是一个引用数据类型的变量。
public class Test {
public static void main(String[] args) {
Person p = new Person();
System.out.println(p);
System.out.println(p);
System.out.println(p);
//p这个对象可以被反复使用
/*
这里的p是对象名,同时也变量名
Java的数据类型:
(1)基本数据类型:byte,short,int,long,float,double,char,boolean
int a = 1;
(2)引用数据类型:数组、类等
int[] arr = new int[5];
Person p = new Person();
int[],Person是数据类型,从这个角度来说,arr, p也是变量。
因为这种变量引用的是一个对象,所以这个变量称为引用数据类型的变量,也称为对象名。
*/
}
}
二、继承
什么是继承
java中继承的意义是实现代码的复用,扩展以及事物之间is-a的关系。
如何实现继承
语法格式:
【修饰符】 class 子类名 extends 父类名{
//子类的成员
}
public class Person { //父类
public String name;
public int age;
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
public void eat(){
System.out.println(name +"在吃东西");
}
}
//Student是子类
public class Student extends Person{
public int score;
public String getInfo(){
return super.getInfo() +",成绩:" + score;
//super.getInfo()是复用父类的方法
}
public void exam(){
System.out.println(name+"在参加考试");
}
}
public class TestStudent { //测试类
public static void main(String[] args) {
Student s = new Student();
s.name = "张三"; //从父类继承的属性声明
s.age = 23;//从父类继承的属性声明
s.score = 89;//Student子类自己扩展的属性
System.out.println(s.getInfo());
s.eat();//从父类继承的方法声明
s.exam();//子类自己扩展的方法
}
}
继承的特点和要求
继承属性和方法
父类的所有成员变量和成员方法代表的事物特征都会继承到子类中。但是,父类中私有的成员变量和成员方法在子类中是无法直接使用的,需要间接使用。
public class Person {
private String name; //私有化
public int age;
public String getName(){
return name;
}
}
public class Student extends Person{
public void exam(){
//System.out.println(name +"现在" + age + "岁,正在参加考试");
//上面的语句报错,父类name加private就不能在子类中直接使用了
System.out.println(getName() +"现在" + age + "岁,正在参加考试");
//子类可以通过getName()方法间接使用name。age没有加private,子类就可以直接使用
}
}
不会继承父类的构造器
子类不会直接继承父类的构造器,子类的构造器首行必须调用父类的构造器。调用父类构造器的目的是为了借用父类构造器的代码为从父类继承的成员变量进行初始化赋值。
- 默认情况下,会调用父类的无参构造。
- 可以手动调用父类的有参构造。
此时就需要两句代码:
- super() :调用父类的无参构造。这句代码可以省略。
- super(实参列表):明确调用父类的有参构造。这句代码不可以省略。
- 它们必须在子类构造器的首行。
情况一:不能借调父类构造器的情况
public class Person {
private String name;
public int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}
public class Student extends Person{
public int score;
public String getInfo(){
return super.getInfo() +",成绩:" + score;
//super.getInfo()是复用父类的方法
}
}
public class TestStudent3 {
public static void main(String[] args) {
Student s1 = new Student();
//因为Student类没有编写任何构造器,编译器自动添加了无参构造
Student s2 = new Student("王五",25);//报错
//Student类没有写有参构造器,编译器不会自动添加,也不会从父类继承有参构造
}
}
情况二:调用父类构造器的情况
public class Person {
private String name;
public int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
public void eat(){
System.out.println(name +"在吃东西");
}
}
public class Student extends Person{
public int score;
public Student() {
super();//调用父类无参构造,可以省略
}
public Student(String name, int age, int score) {
super(name, age);//调用父类的有参构造
this.score = score;
}
public String getInfo(){
return super.getInfo() +",成绩:" + score;
//super.getInfo()是复用父类的方法
}
public void exam(){
//System.out.println(name +"现在" + age + "岁,正在参加考试");
//上面的语句报错,父类name加private就不能在子类中直接使用了
System.out.println(getName() +"现在" + age + "岁,正在参加考试");
//子类可以通过getName()方法间接使用name。age没有加private,子类就可以直接使用
}
}
public class TestStudent4 {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student("王五",25,100);
}
}
子类可以重写父类的方法
当子类从父类继承了一个方法,但是该方法的实现(即方法体)不完全适用于子类,那么子类就可以对其进行重写。
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}
public class Student extends Person{
public int score;
public Student() {
super();//调用父类无参构造,可以省略
}
public Student(String name, int age, int score) {
super(name, age);//调用父类的有参构造
this.score = score;
}
//此时去掉@Override,getInfo方法也是重写
/*
加@Override的意义:
(1)可读性更好。程序员一看到它,就知道这是一个重写的方法
(2)可以让编译器,对getInfo()方法做严格的格式检查,看它有没有违反重写的要求。
如果你没有违反重写的要求,那么不加@Override也没有影响。
*/
@Override //标记当前的getInfo方法是重写父类的getInfo方法,不是子类新增的方法
public String getInfo(){
return super.getInfo() +",成绩:" + score;
//super.getInfo()是复用父类的方法
}
}
方法重载和重写的区别
重载(Overload) | 重写(Override) | |
---|---|---|
位置 | 同一个类中或 | 重写的方法是在子类中,被重写的方法在父类中 |
①权限修饰符 | 不看 | (1)被重写方法不能是private (2)重写方法的权限修饰符必须 >= 被重写方法的权限修饰符 |
①其他修饰符 | 不看 | 被重写方法不能是static |
②返回值类型 | 不看 | (1)当返回值类型是void或8种基本数据类型,那么重写方法的返回值类型必须与被重写方法的返回值类型完全一致。 (2)当返回值类型是引用数据类型,那么重写方法的返回值类型必须 <= 被重写方法的返回值类型 |
③方法名 | 必须相同 | 必须相同 |
④形参列表 | 必须不同 | 必须相同(只看类型、个数、顺序,不看形参名) |
Object类toString的重写
Java的所有类都直接或间接的继承java.lang.Object类。Object类中有11个方法,这11个方法会被继承到所有子类中。其中非常好用,常用的一个方法就是toString方法。
- 这个方法比较特殊,当我们打印对象时,或者对象与字符串拼接时,都会自动调用对象的toString方法。
- 建议所有子类都重写toString方法。否则默认返回 类名 @ 对象哈希值的无符号十六进制值。
- 可以通过快捷键自动生成toString方法,快捷键是Alt + Insert。
public class Animal {
private String name;
private double weight;
/* public String toString(){ //手动重写
return "名称:" + name +",体重:" +weight;
}*/
@Override
public String toString() {//快捷键生成
return "Animal{" +
"name='" + name + '\'' +
", weight=" + weight +
'}';
}
}
public class TestAnimal {
public static void main(String[] args) {
Animal a = new Animal();
System.out.println(a);//自动调用a的toString方法
System.out.println(a.toString());
/*
如果没有重写,打印的是如下信息:
com.yang.inherited.Animal@4eec7777
类名 @ 对象的hash码的无符号十六进制形式
*/
System.out.println(a.hashCode());
//1324119927(十进制) -> 4eec7777(十六进制)
}
}
super关键字
super:引用父类的xxx
常见的用法有3种:
(1)super.方法(【实参列表】)
想要在子类中调用父类被重写方法时,必须加 “super.”。
(2)super() 或 super(实参列表)
- super():明确调用父类的无参构造。这句可以省略。
- super(实参列表):明确调用父类的有参构造
- 它们都必须在子类构造器的首行
3)super.实例变量
当子类声明与父类同名的实例变量时,如果两个实例变量都可见,那么在子类中可以通过“super.实例变量”来访问父类的实例变量。
但是,不推荐父子类声明同名的实例变量。
this | super | |
---|---|---|
意思 | 当前对象 | 父类声明的xx |
调用方法 | this.实例方法 完全可以省略this. | super.实例方法 不可以省略super.,除非你没重写该方法。 |
使用成员变量 | this.成员变量 成员变量与局部变量重名,用它。 | super.成员变量 父子类成员变量重名,用它。 |
调用构造器 | this() 或 this(实参列表) 调用本类的构造器 | super() 或 super(实参列表) 调用父类的构造器 |
说明:this() 、 this(实参列表)、 super() 、super(实参列表) 这四句不能同时出现在一个构造器中。只能四选一。如果四句都没写,默认就是super()。
三、多态
什么是多态
在Java中多态的含义:一个对象的方法有多种形态(即多种声明方式)
- 方法的重载:编译时多态
- 方法的重写:运行时多态
运行时多态的好处:可以实现方法的动态绑定,即给父类变量赋值不同的子类对象,可以动态的执行子类各自重写的方法。
运行时多态
多态引用语法:
父类类型 变量名 = 子类对象;
多态引用时调用方法有一个原则:
编译看父类,运行看子类。
- 只要子类重写了所调用的方法,就执行子类重写的方法体。
- 如果子类没有重写所调用的方法,仍然去父类找方法执行。
public class Animal {
public void eat(){
System.out.println("~~~~");
}
public void eat(String food){
System.out.println("吃:" + food);
}
}
public class Dog extends Animal{
@Override
public void eat() {
System.out.println("啃骨头");
}
public void watchHouse(){
System.out.println("看家");
}
}
public class TestDog {
public static void main(String[] args) {
Animal obj = new Dog();
//obj编译时看左边,obj编译时看的是Animal类型中的方法
//obj运行时看右边,obj运行时看的是Dog类型中的方法
obj.eat(); //执行Dog重写的eat()方法
obj.eat("杂食");
//执行先找Dog类是否重写了eat(String food),如果重写了,就会执行Dog类重写的eat(String food)
//如果没有重写,仍然执行父类继承的eat(String food)
// obj.watchHouse();
//编译报错,因为编译看左边,Animal类中没有watchHouse方法
}
}
运行时多态的应用
多态数组
可以用一个父类/父接口类型的数组来管理一组子类的对象,统一管理它们相同的行为特征。
数组的类型是父类[],实际存储的是子类对象。
public class Animal {
public void eat(){
System.out.println("~~~~");
}
public void eat(String food){
System.out.println("吃:" + food);
}
}
public class Animal {
public void eat(){
System.out.println("~~~~");
}
public void eat(String food){
System.out.println("吃:" + food);
}
}
public class Dog extends Animal{
@Override
public void eat() {
System.out.println("啃骨头");
}
public void watchHouse(){
System.out.println("看家");
}
}
public class Cat extends Animal{
@Override
public void eat() {
System.out.println("吃老鼠");
}
public void catchMouse(){
System.out.println("抓老鼠");
}
}
public class TestAnimals {
public static void main(String[] args) {
//现在有一组动物对象,它们分别是Dog、Cat、Bird类的对象
Animal[] animals = new Animal[3];
animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Bird();
//声明:animals[0]、animals[1]、animals[2]元素的类型是Animal类型
//赋值:animals[0]、animals[1]、animals[2]分别赋值Animal类的子类对象
//变相的多态引用 Animal animals[0] = new Dog();
for (Animal animal : animals) {
animal.eat();
//编译时看左边,或看父类, animal此时只能.出Animal类中声明的eat()和eat(String food)
//运行时看右边,或看子类, animal.eat()实际执行的都是子类重写的eat()
}
}
}
多态参数
方法的形参是父类类型,实参是子类对象。
public class TestAnimals2 {
public static void main(String[] args) {
look(new Cat());
/*
look方法的形参(Animal animal)
look方法的实参(new Cat())
变相的多态引用 Animal animal = new Cat();
*/
Dog d = new Dog();
look(d);//有名字的对象作为实参
look(new Bird());
}
//定义一个方法,可以看各种动物吃东西的行为
public static void look(Animal animal){
animal.eat();
}
/* public static void look(Dog dog){
//System.out.println("看狗的方法:");
dog.eat();
}
public static void look(Cat cat){
// System.out.println("看猫的方法:");
cat.eat();
}
public static void look(Bird bird){
// System.out.println("看鸟的方法:");
bird.eat();
}*/
}
多态返回值
方法的返回值类型是父类类型,实际返回的是子类对象。
public class TestAnimals3 {
public static void main(String[] args) {
Animal dog = buy("狗");
//因为这个方法实际返回的是new Dog()
//变相的多态引用 Animal dog = new Dog();
dog.eat();
}
public static Animal buy(String type){
switch (type){
case "猫": return new Cat();
case "狗": return new Dog();
case "鸟": return new Bird();
}
return null;
}
/*public static Dog buyDog(){
return new Dog();//直接返回匿名对象
}
public static Cat buyCat(){
Cat c = new Cat();
return c;//直接返回有名对象
}
public static Bird buyBird(){
return new Bird();//直接返回匿名对象
}*/
}
向上转型与向下转型
自动类型转换:
char c = 'a';
double d = c; //这里是把c变量中的'a'的编码值copy一份,然后再自动提升为double类型
强制类型转换:
double d = 97.5;
char c = (char)d;
向上和向下转型是指父类和子类之间类型的转换
向上转型:让子类对象在编译时以父类的类型出现。一旦向上转型后,就会失去子类自身的特有方法,只能调用父类中有的方法。
向下转型:让一个父类对象在编译的时候以子类的类型出现。
注意:①在向下类型转型的时候有风险,可能出现ClassCastException(类型转换异常)。
②除非父类中变量引用的就是当前子类的对象,否则就会发生ClassCastException。
③为了安全起见,向上转型之前最好用 instanceof判断。用于判断某个变量中是否是某个类型的对象。
无论是向上转型,还是向下转型,对象的本质类型(运行时类型)从头到尾都没有变过,就是你new的类型。
public class TestAnimals2 {
public static void main(String[] args) {
//用一个数组管理一组动物对象
Animal[] animals = new Animal[3];
animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Bird();
for (Animal animal : animals) {
animal.eat();
//这里不仅想要调用 它们公共的 eat(),
// 还想要调用Dog的watchHouse(),
// Cat的catchMouse(),
// Bird的fly()
if(animal instanceof Dog) {
Dog d = (Dog) animal;
d.watchHouse();
}else if(animal instanceof Cat){
Cat c = (Cat) animal;
c.catchMouse();
}else if(animal instanceof Bird){
Bird b = (Bird) animal;
b.fly();
}
}
}
}
四、封装
封装(Encapsulation)是面向对象编程的三大基本特性之一,它指的是将数据(属性)和行为(方法)捆绑在一起,形成一个对象,并且只通过公开的接口来访问这个对象。封装的目的是为了保护对象的内部状态,防止外部直接访问和修改对象的数据,确保数据的完整性和程序的安全性。
在Java这样的面向对象语言中,封装通常通过使用访问修饰符来控制。例如,使用private关键字修饰的属性或方法只能在类的内部访问,而使用public关键字修饰的属性或方法则可以从类的任何外部访问。
下面是一个简单的Java代码例子,展示了封装的概念:
public class Car {
// 私有属性,不能直接从类的外部访问
private String color;
private int speed;
// 公开的方法,用于访问私有属性
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
if (speed >= 0 && speed <= 120) { // 限制速度的有效性
this.speed = speed;
} else {
System.out.println("Invalid speed!");
}
}
// 公开的方法,执行操作(如开车)
public void drive() {
System.out.println("The car is driving with speed " + speed + " km/h.");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
// 通过公共方法设置车辆颜色
myCar.setColor("Red");
// 通过公共方法获取车辆颜色
System.out.println("Car color: " + myCar.getColor());
// 通过公共方法设置车辆速度
myCar.setSpeed(60);
// 通过公共方法执行驾驶操作
myCar.drive();
}
}
在这个例子中,Car类有两个私有属性 color 和 speed,以及几个公开的方法。这些公开的方法提供了控制车辆属性的安全接口。例如,setColor方法用于设置车辆的颜色,而 setSpeed
方法用于设置车辆的速度,同时检查速度是否在合理的范围内。这种方式确保了外部代码不能直接访问或修改 color 和 speed属性,必须通过 getColor、setColor、getSpeed 和 setSpeed这些公共方法来间接访问。这样的封装保证了类的内部状态不会被非法操作,同时也保护了代码的完整性。