面向对象
什么是面向对象
以往面向过程的思想,思路比较清晰简单,适合处理加单的问题。
面向对象(Object Oriented Programming)先考虑解决问题需要分哪些类,适合处理复杂的、多人协作的问题。
-
其本质是——以类的方式组织代码,以对象的方式封装数据。
-
抽象
-
三大特性:封装、继承、多态
对象是具体的事物,类是抽象的事物。
代码先定义类,再实例化为对象。
类与对象的创建
创建一个Student类:
public class Student { //类与对象的创建 String name; int age; public void study(){ System.out.println(name + "在学习!"); } }
其中包含变量姓名、年龄,与方法学习。
实例化两个学生:
public static void main(String[] args) { Student xiaoming = new Student(); Student xiaohong = new Student(); }
给小明初始化,并打印:
xiaoming.name = "小明"; xiaoming.age = 13; System.out.println(xiaoming.name); System.out.println(xiaoming.age);
当然,正常不需要一条条初始化,可以用构造器初始化。
构造器
实例化对象的时候,一定会调用构造方法,即构造器,
如果没有定义,那么会默认调用空的构造器:
public Student() { }
如实例化小明时,要同时传入其属性name、age的默认值,则需要有参构造器:
public Student(String name, int age) { this.name = name; this.age = age; }
定义了有参构造器,就默认不能无参实例化了,除非把空的构造器自己写一遍。
我们用有参构造器重新实例化一下小红:
Student xiaohong = new Student("小红",14);
这样就不需要一个个赋值了,可以打印出来看一下。
构造器的快捷键:Alt + Insert。
构造器两个特点:
-
必须与类的名字相同
-
必须没有返回值
构造器也可以定义多个进行重载。
各种数据类型的默认值:
-
int:0
-
boolean:false
-
char:u0000
-
引用类型:null
对象的属性:student.name
对象的方法:student.study()
注意对象一般是小写开头。
封装
一个类对其中的属性进行封装,一般使用private声明,要查询或更改属性的值,
一般要通过getter、setter方法来实现。
封装就是黑盒子,使用者不必知道类中的结构,只需要知道如何使用!
高内聚低耦合:大部分数据操作封装起来自己搞,只留少量方法供外部调用。
封装示例:
public class PrivateStudent { //封装 private String name; private int id; private boolean Male; public PrivateStudent(String name, int id, boolean gender) { this.name = name; this.id = id; this.Male = gender; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public boolean isMale() { return Male; } public void setMale(boolean male) { this.Male = male; } @Override public String toString() { return "PrivateStudent{" + "name='" + name + '\'' + ", id=" + id + ", Male=" + Male + '}'; } }
看起来挺多东西,其实只有属性是自己敲的,后面的都是Alt + Insert里面自动生成的。
有构造器、gettersetter、toString方法(这个自动生成要生成无参构造器要再点一遍)。
调用一下:
public static void main(String[] args) { PrivateStudent student = new PrivateStudent("小明", 1, true); System.out.println(student.toString()); student.setName("小红"); student.setId(10); student.setMale(false); System.out.println(student.toString()); System.out.println(student.getName()); System.out.println(student.getId()); System.out.println(student.isMale()); }
先new一个小明,使用toString方法打印,在改成小红,
并在打印后使用了一下getter方法。
输出结果:
PrivateStudent{name='小明', id=1, Male=true} PrivateStudent{name='小红', id=10, Male=false} 小红 10 false
为什么要做看起来多此一举的封装呢?因为可以在setter里面做安全性检查,
比如setAge中,可以要求年龄不小于0岁才让set,就不会让属性被随便操作。
列举封装的优点:
-
提高安全性,保护数据
-
隐藏实现细节
-
统一接口
-
提高可维护性
继承
继承的关键词是extends,意思是扩展,子类是父类的扩展。
Java中只有单继承,即一个子类只能继承一个父类。
子类继承了父类,就会拥有父类的全部方法:
public class Animal { public void isAnimal(){ System.out.println("是动物"); } }
建立一个Animal类,其中有一个方法isAnimal,
创建一个Cat类继承他:
public class Cat extends Animal{ }
在main方法中new一个cat,并调用其父类的方法:
public static void main(String[] args) { Cat cat = new Cat(); cat.isAnimal(); }
可以打出“是动物”,即Cat类中虽然没有这个方法,但可以使用父类的同名方法。
此外,父类中的属性也同样可以继承。
但是private是继承不了的,protected是子类可以继承,别人不能调用的,
public是谁都可以调用的。
快捷键F4可以打开继承树。
注意:在Java中,所有的类默认继承Object类(直接或间接)。
使用super关键词可以调用父类的变量,先在Animal中定义一个变量:
protected String name = "动物";
在Cat中加些东西:
private String name = "猫猫"; public String getName() { return name; } public void setName(String name) { this.name = name; } public void test(String name){ System.out.println(name); System.out.println(this.name); System.out.println(super.name); }
主程序调用:
cat.test("周六");
输出“周六、猫猫、动物”,即方法test中:
name是传参,this.name是Cat类中的名字,super.name是父类中的名字。
当然了super调用不了private的东西。
构造器也可以用super继承:
public Animal(String name) { this.name = name; }
Cat中:
public Cat(String name) { super(name); }
主程序实例化时:
Cat cat = new Cat("周六");
这样Animal的name就被构造器变成周六了,而Cat的name仍是猫猫。
方法的重写
重写和重载不一样,重写是子类重新写一遍父类的同名方法,调用子类的这个方法时,执行子类重写的部分。
新建一个包package06。
(重写都是方法的重写,跟属性无关)
写一个B类:
public class B{ public static void test(){ System.out.println("B=>test"); } }
写一个A类继承B类:
public class A extends B{ public static void test(){ System.out.println("A=>test"); } }
主程序测试一下:
public static void main(String[] args) { A a = new A(); a.test(); B b = new A(); b.test(); }
发现可以new一个子类A,定义为父类B,调用其中静态方法时,调用的是B类的方法。
将B类的static去掉,A类整个方法去掉,改为:
@Override public void test() { System.out.println("A=>test"); }
快捷键Alt + Insert,生成的是默认的。
再运行主程序,发现结果不同了,两次输出的都是A=>test,
这是因为重写跟静态方法无关,而现在是非静态方法,子类重写了父类的test。
private方法无法重写。
为什么重写:
-
父类的功能子类不需要或不满足
多态
一个对象的实际类型是确定的,如new Student,就是Student类:
Student student = new Student();
而可以指向的引用类型就不确定了:
Person person = new Student();
前提是Student继承了Person。
对象能执行哪些方法,看定义对象时左边的类型。
父类型指向子类,不能调用子类独有的方法。
多态注意事项:
-
多态是方法的多态
-
要存在继承关系
-
只能是父类引用指向子类
instenceof和类型转换
instanceof判断两个类是否存在继承关系。
用上节的包,在主程序里测试:
Object object = new A(); System.out.println(object instanceof A); System.out.println(object instanceof B); System.out.println(object instanceof Object);
输出结果,三个true。
类型转换,跟前面的数据类型一样,在前面加个括号即可。
抽象类
abstract关键字,用来修饰类就是抽象类,用来修饰方法就是抽象方法。
public abstract class Action { //抽象方法,没有方法体 public abstract void doSomething(); }
没有方法体就是留给子类实现的,相当于一个约束,
即抽象类的子类必须实现其中所有的抽象方法,除非子类也是抽象类。
抽象类特点:
-
不能new出来,只是给子类的约束
-
有抽象方法的类只能说是抽象类,但其中可以有普通方法
抽象类可以提高开发效率。
接口的定义与实现
-
普通类:只有具体实现
-
抽象类:具体实现和规范都有
-
接口:只有规范
面向对象的精髓,就是对对象的抽象,设计模式研究的就是如何合理地抽象。
接口中的方法默认都是public abstract的,属性默认都是public static final的常量。
建立一个接口UserService:
public interface UserService { void add(); void delete(); void update(); void query(); }
定义一个类去实现他:
public class UserServiceImpl implements UserService{ @Override public void add() { } @Override public void delete() { } @Override public void update() { } @Override public void query() { } }
不同于抽象类,这里使用的是implements关键字,不是继承是实现!
最大区别是一个类可以实现多个接口,用逗号分隔即可。
内部类
在类里面写一个类就行内部类了,不过分为很多种。
-
成员内部类
-
静态内部类
-
局部内部类
-
匿名内部类
成员内部类与静态内部类:
public class Outer { public void out(){ System.out.println("外部类"); } public class Inner1{ public void in(){ System.out.println("成员内部类"); } } public static class Inner2{ public void in(){ System.out.println("静态内部类"); } } }
实例化:
public static void main(String[] args) { Outer outer = new Outer(); Outer.Inner1 inner1 = outer.new Inner1(); Outer.Inner2 inner2 = new Outer.Inner2(); outer.out(); inner1.in(); inner2.in(); }
实例化内部类的方法比较特殊啊。
局部内部类就是在外部类的方法里面的类。
匿名内部类就是不把实例保存到变量中,直接new出来点方法。
异常
Error和Exception
程序运行中可能遇到一些异常,异常处理机制是让我们的程序在遇到异常时不至于崩溃。
public static void main(String[] args) { new Demo01().a(); } public void a(){ b(); } public void b(){ a(); }
程序无限循环调用,会栈溢出,出现StackOverflowError。
public static void main(String[] args) { System.out.println(11/0); }
0作为除数,会出现ArithmeticException异常。
三种类型的异常:
-
检查性异常:程序员无法预见,如用户要打开一个文件。但文件不存在。
-
运行时异常:程序员可以预见,编译时可以被忽略。
-
错误:错误不是异常,他是程序脱离程序员控制,如栈溢出。
Java语言设计之初就考虑到这些问题,可以将这些异常当作对象来处理,具有如下结构:
Error是由JVM生成并抛出的,大多数与代码执行者的操作无关。
运行时异常一般是程序逻辑错误导致的,程序员应尽量避免此类异常的发生。
Error和Exception的区别,Exception是可以被程序处理的。
异常的捕获和抛出
异常处理有五个关键字:
try、catch、finally、throw、throws
我们对上节除数为0的异常进行一下处理:
public static void main(String[] args) { try { System.out.println(11/0); }catch (ArithmeticException e){ System.out.println("程序出现异常,被除数不能为0"); }finally{ System.out.println("finally"); } }
try下面的代码块是监控区域,后面必须跟catch,监控区域出现了catch后面括号内标明的异常类型,
则会执行catch下面的代码块。
无论是否被捕获到了异常,finally下面的代码块都会被执行,finally不是必须的,但try后面必须有catch。
上节的循环调用栈溢出错误,我们从异常结构图中可以看到,是比Throwable低的,可以用来捕获:
public static void main(String[] args) { try { new Demo01().a(); }catch (Throwable e){ System.out.println("发生了错误!"); } }
如果catch里的参数是要捕获的异常类型。
catch后面还能接catch,上面写小的类型,下面写大的类型,
这样可以在捕获的同时判断异常类型。
异常及其他一些包裹代码的快捷键:Ctrl + Alt + T。
除了捕获异常,我们还可以主动地抛出异常:
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入被除数:"); int num1 = scanner.nextInt(); System.out.println("请输入除数:"); int num2 = scanner.nextInt(); if(num2==0){ throw new ArithmeticException(); }else { System.out.println(num1+"/"+num2+"="+num1/num2); } scanner.close(); }
测试发现,报了ArithmeticException,即使将后面的除法运算注释掉,
还是会报这个错,这是因为num2等于0时我们主动抛出了这个异常。
抛出异常的为了处理和检查异常。