1.封装
1.1 概念
举个例子解释,我们使用计算机时,并不关心内部核心部件,而是只需要知道如何开机,如何通过键盘鼠标与计算机进行交互即可.因此厂家在出厂计算机时,在外部套上壳子将内部实现细节隐藏起来,仅仅对外提供开机等,让用户可以与计算机进行交互即可.
封装:
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,进对外公开接口来和对象进行交互.
1.2 访问限定符
java中主要通过类和访问权限来实现封装
No | 范围 | private | default | protected | public |
---|---|---|---|---|---|
1 | 同一包中的同一类 | √ | √ | √ | √ |
2 | 同一包中的不同类 | √ | √ | √ | |
3 | 不同包中的子类 | √ | √ | ||
4 | 不同包中的非子类 | √ |
private:只有自己知道
default:什么都不写时的默认权限,对于自己家族中不是秘密
protected:主要用在继承中
public:谁都可以知道
一般情况下成员变量设置为private,成员方法设置为public
1.3 封装扩展之包
1.3.1 包的概念
在面向对象体系中,提出了一个软件包的概念,即:为了更好的管理类,把多个类收集在一起成为一组,称为软件包.
在Java中也引入了包,包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式.
1.3.2 导入包中的类
方法一:
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
System.out.println(date.getTime());
}
}
方法二:(较为简易)
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date.getTime());
}
}
那么看这个:
import java.util.Arrays;
import java.util.Date;
public class Test1 {
public static void main(String[] args) {
Date date = new Date();
Arrays.sort();
}
}
此时有两个java.util 中的类,我们就可以用 import java.util.*
import java.util.*;
public class Test1 {
public static void main(String[] args) {
Date date = new Date();
Arrays.sort();
}
}
不是导入util下的所有类,作用是用到哪个类,导入哪个类
更建议显式的指定要导入的类名. 否则还是容易出现冲突,比如:
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
// util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
//java.sql 中的类 java.sql.Date 和 java.util 中的类 java.util.Date 都匹配
Date date = new Date();
System.out.println(date.getTime());
}
}
此时在这种情况下需要使用完整的类名
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
System.out.println(date.getTime());
}
}
所以*非必要不要用
可以使用import static导入包中静态的方法和字段。(不建议)
import static java.lang.Math.*;
public class Test {
public static void main(String[] args) {
double x = 30;
double y = 40;
// 静态导入的方式写起来更方便一些.
//代码上看起来简短一点
// double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
double result = sqrt(pow(x, 2) + pow(y, 2));
System.out.println(result);
}
}
1.3.3 自定义包
包其实就是Java当中组织类的一种方式
基本规则
- 在文件的最上方加上一个 package 语句指定该代码在哪个包中.
- 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式,比如:com.bit,包名一定要全部小写
- 包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码.
- 如果一个类没有 package 语句, 则该类被放到一个默认包中.
1.3.4 包的访问权限控制举例
详见0318第六,七次测试
生成封装方法
public class Test2 {
public static void main(String[] args) {
Student student = new Student();
student.setName("小笨猪");
student.setAge(19);
System.out.println(student.getName());
System.out.println(student.getAge());
}
}
class Student{
private String name;
private int age;
public Student(){
System.out.println("不含参数的构造方法");
}
public Student(String name, int age) {
this.name = name;
this.age = age;
System.out.println("含有两个参数的构造方法");
}
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
public void print(){
System.out.println("姓名:"+this.name+" 年龄: "+this.age);
}
}
那么我们要一个一个去收到设置属性嘛,当然不是,idea会帮我们生成
2. 继承
2.1 概念
比如说,猫和狗都是动物,而它们的属性和行为存在大量重复,那么就可以将这些共性抽取,实现代码复用,这就是继承.
看下图所示:
其中,继承之后,子类可以服用父类中成员,子类在实现时只需要关心自己新增的成员即可.
2.2 语法
表示类之间的继承关系,借助extends关键字
子类继承父类之后,必须要新添加自己特有的成员,体现出与父类的不同.
2.3 父类成员访问
2.3.1 子类中访问父类的成员变量
2.3.1.1 子类和父类不存在同名成员变量
public class Base {
int a;
int b;
}
public class Derived extends Base{
int c;
public void method(){
a = 10;//访问从父类中继承下来的a
b = 10;//访问从父类中继承下来的b
c = 30;//访问子类自己的c
}
}
2.3.1.2 子类和父类成员变量同名
public class Base {
int a;
int b;
}
public class Derived extends Base{
int a; //与父类中成员a相同,且类型相同
char b; //类型不同
public void method(){
a = 10;//访问子类自己的a
b = 10;//访问子类自己的b
c = 30;//子类父类都没有,编译失败
}
}
注:(就近原则)
①如果访问的成员变量子类中有,优先访问自己的成员变量
②如果访问的成员变量子类中无,则访问从父类继承下来的,如果父类也没有,编译失败
③ 如果访问的成员变量与父类中成员变量同名,则优先访问自己的
2.3.2 子类中访问父类的成员方法
2.3.2.1 成员方法名字不同
public class Base {
public void methodA(){
System.out.println("Base中的methodA()");
}
}
public class Derived extends Base{
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
methodB(); // 访问子类自己的methodB()
methodA(); // 访问父类继承的methodA()
// methodD(); // 编译失败,在整个继承体系中没有发现方法methodD()
}
}
2.3.2.2 成员方法名字相同
public class Base {
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
public void methodA(int a) {
System.out.println("Derived中的method(int)方法");
}
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
methodA(); // 没有传参,访问父类中的methodA()
methodA(20); // 传递int参数,访问子类中的methodA(int)
methodB(); // 直接访问,则永远访问到的都是子类中的methodB(),基类的无法访问到
}
}
注:
- ①通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到 则访问,否则编译报错.
- ②通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用 方法适传递的参数选择合适的方法访问,如果没有则报错.
2.4 super关键字
当子类和父类中存在相同名称的成员,如果要在子类方法中访问父类同名成员时,直接访问是无法做到的,此时Java提供了super关键字.
主要作用:在子类方法中访问父类的成员.
public class Base {
int a;
int b;
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
int a; // 与父类中成员变量同名且类型相同
char b; // 与父类中成员变量同名但类型不同
// 与父类中methodA()构成重载
public void methodA(int a) {
System.out.println("Derived中的method()方法");
}
// 与基类中methodB()构成重写
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
// 对于同名的成员变量,直接访问时,访问的都是子类的
a = 100; // 等价于: this.a = 100;
b = 101; // 等价于: this.b = 101;
// 注意:this是当前对象的引用
// 访问父类的成员变量时,需要借助super关键字
// super是获取到子类对象中从基类继承下来的部分
super.a = 200;
super.b = 201;
// 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
methodA(); // 没有传参,访问父类中的methodA()
methodA(20); // 传递int参数,访问子类中的methodA(int)
// 如果在子类中要访问重写的基类方法,则需要借助super关键字
methodB(); // 直接访问,则永远访问到的都是子类中的methodA(),基类的无法访问到
super.methodB(); // 访问基类的methodB()
}
}
注:
①只能在非静态方法中使用
②在子类方法中,访问父类的成员变量和方法
2.5 子类构造方法
子类对象构造时,需要先调用基类构造方法,然后执行子类的构造方法
public class Base1 {
public Base1() {
System.out.println("Base1()");
}
}
public class Derived1 extends Base1{
public Derived1() {
System.out.println("Derived2()");
}
// super(); // 注意子类构造方法中默认会调用基类的无参构造方法:super(),
// 用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,
// 并且只能出现一次
public static void main(String[] args) {
Derived1 derived1 = new Derived1();
}
}
在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整 ,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整.
注:
1. 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法.
2. 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败.
3. 在子类构造方法中,super(...)调用父类构造时,必须是子类构造函数中第一条语句.
4. super(...)只能在子类构造方法中出现一次,并且不能和this同时出现.
2.6 super和this
2.7 继承方式
Java中支持:
不支持:
2.8 final关键字
final关键可以用来修饰变量、成员方法以及类
①修饰变量或字段,表示常量(即不能修改)
②修饰类:表示此类不能被继承
2.9 继承与组合
能用组合尽量用组合.
比如:
汽车和其轮胎、发动机、方向盘、车载系统等的关系应该是组合,汽车是由这些部件组成的.
3.多态
3.1 概念
多态即多种形态.
即就是去完成某个行为,不同的对象去完成时会产生出不同的状态.
3.2 多态实现条件
① 必须在继承体系下
② 子类必须要对父类中方法进行重写
③ 通过父类的引用调用重写的方法
public class Animal {
String name;
int age;
public Animal(String name, int age){
this.name = name;
this.age = age;
}
public void eat(){
System.out.println(name + "吃饭");
}
}
public class Cat extends Animal{
public Cat(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃鱼~~~");
}
}
public class Dog extends Animal {
public Dog(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃骨头~~~");
}
}
public class TestAnimal {
public static void eat(Animal a){
a.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("xx",2);
Dog dog = new Dog("mm",3);
eat(cat);
eat(dog);
}
}
解释:
当类的调用者在编写 eat 这个方法的时候, 参数类型为 Animal (父类), 此时在该方法内部并不知道, 也不关注当前的 a 引用指向的是哪个类型(哪个子类)的实例. 此时a这个引用调用eat方法可能会有多种不同的表现(和 a 引用的实例相关), 这种行为就称为多态.
3.3 重写
- 重写(覆盖)是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
- 重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
3.3.1 方法重写的规则
- 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表)
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
- 父类被static、private修饰的方法、构造方法都不能被重写。
- 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心 将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
3.3.2 重写和重载的区别
即:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现.
3.4 向上转移和向下转型
3.4.1 向上转型
实际就是创建一个子类对象,将其当成父类对象来使用.
语法格式:父类类型 对象名 = new 子类类型()
优点:让代码实现更简单灵活.
缺陷:不能调用到子类特有的方法.
public class TestAnimal {
// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象
public static void eatFood(Animal a){
a.eat();
}
// 3. 作返回值:返回任意子类对象
public static Animal buyAnimal(String var){
if("狗".equals(var) ){
return new Dog("狗狗",1);
}else if("猫" .equals(var)){
return new Cat("猫猫", 1);
}else{
return null;
}
}
public static void main(String[] args) {
Animal cat = new Cat("元宝",2); // 1. 直接赋值:子类对象赋值给父类对象
Dog dog = new Dog("小七", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("猫");
animal.eat();
}
}
3.4.1 向下转型
将一个子类对象经过向上转型之后当成父类方法使用,就无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换.(不安全)
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
// 向上转型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
// 编译失败,编译时编译器将animal当成Animal对象处理
// 而Animal类中没有bark方法,因此编译失败
// animal.bark();
// 向上转型
// 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗
// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
cat = (Cat)animal;
cat.mew();
// animal本来指向的就是狗,因此将animal还原为狗也是安全的
dog = (Dog)animal;
dog.bark();
}
}
3.5 多态的优缺点
用下面代码为例:
class Shape {
//属性....
public void draw() {
System.out.println("画图形!");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.println("●");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("❀");
}
}
3.5.1 好处
①能够降低代码的 "圈复杂度", 避免使用大量的 if - else
如果要打印多个形状,不基于多态:
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
如果基于多态:
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
② 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
3.5.2 缺陷
①属性没有多态性
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性.
②构造方法没有多态性.
3.6 避免在构造方法中调用重写的方法
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
// 执行结果
D.func() 0
所以说,尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.