面向对象的思想

面向过程思想概述

  • 我们来回想一下,这几天我们完成一个需求的步骤:首先是搞清楚我们要做什么,然后在分析怎么做,最后我们再代码体现。
  • 一步一步去实现,而具体的每一步都需要我们去实现和操作。这些步骤相互调用和协作,完成我们的需求。在上面的每一个具体步骤中我们都是参与者,并且需要面对具体的每一个步骤和过程,这就是面向过程最直接的体现。
  • 那么什么是面向过程开发呢?
    • 面向过程开发,其实就是面向着具体的每一个步骤和过程,把每一个步骤和过程完成,然后由这些功能方法相互调用,完成需求。 面向过程的代表语言:C语言

面向对象思想概述

  • 当需求单一,或者简单时,我们一步一步去操作没问题,并且效率也挺高。

  • 可随着需求的更改,功能的增多,发现需要面对每一个步骤很麻烦了。这时就开始思索,能不能把这些步骤和功能在进行封装,封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰了很多。用的时候,找到对应的类就可以了。这就是面向对象的思想。

  • 面向对象思想特点

    • 是一种更符合我们思想习惯的思想
    • 可以将复杂的事情简单化
    • 将我们从执行者变成了指挥者,角色发生了转换
  • 面向对象有三个特征

    • 封装

      • 将对象的属性和实现细节隐藏起来,只提供公共的访问方式。
    • 继承

      • 继承是从已有的类派生出新的类,新的类能继承已有类的数据属性和行为,并扩展新的功能。
      • java支持单继承,译为多继承存在安全隐患(当多个父类存在相同功能时,子类不确定要运行那个)
      • java支持多层继承,即父类还可以继承其他的类。java用另外一种机制解决了单继承的问题,即多实现。
    • 多态

      • 继承让类与类之间产生了关系,有了关系才有了多态的实现。
      • 允许不同类型的子对象对统一消息做出不同的响应。
    • 抽象

      • Java关于抽象,最常被讨论的就是abstract类和interfaces
      • 抽象是把系统中需要处理的数据和在这些数据上的操作结合在一起,根据功能、性质和用途等因素抽象成不同的抽象数据类型。每个抽象数据类型既包含了数据,又包含了针对这些数据的授权操作。在面向对象的程序设计中,抽象数据类型是用“类”这种结构来实现的。

六大原则

单一职责原则

  • 指一个类的功能要单一,不能包罗万象。

开放封闭原则

  • 指一个模块在扩展性方面应是开放的,在更改性方面应是封闭的

替换原则

  • 子类应当可以替换父类,并出现在父类能够出现的任何位置

依赖原则

  • 具体依赖抽象,上层依赖下层

接口分离原则

  • 模块间要通过抽象接口隔开,而不是通过具体的类强行耦合起来。

迪米特法则

  • 又称最少知道原则(Demeter Principle,DP)
    • 最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

实践案例分析

将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展?

  • 看看下面这段代码,改编某伟大公司产品代码,你觉得可以利用面向对象设计原则如何改进?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class VIPCenter {
    void serviceVIP(T extend User user>) {
    if (user instanceof SlumDogVIP) {
    // 穷 X VIP,活动抢的那种
    // do somthing
    } else if(user instanceof RealVIP) {
    // do somthing
    }
    // ...
    }
    • 这段代码的一个问题是,业务逻辑集中在一起,当出现新的用户类型时,比如,大数据发现了我们是肥羊,需要去收获一下, 这就需要直接去修改服务方法代码实现,这可能会意外影响不相关的某个用户类型逻辑。
    • 利用开关原则,可以尝试改造为下面的代码。将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class VIPCenter {
    private Map<User.TYPE, ServiceProvider> providers;
    void serviceVIP(T extend User user) {
    providers.get(user.getType()).service(user);
    }
    }

    interface ServiceProvider{
    void service(T extend User user) ;
    }

    class SlumDogVIPServiceProvider implements ServiceProvider{
    void service(T extend User user){
    // do somthing
    }
    }

    class RealVIPServiceProvider implements ServiceProvider{
    void service(T extend User user) {
    // do something
    }
    }

类和对象

  • 对象:实体, 一切可以被描述的事物,是该类事物的具体体现

    • 类 学生
    • 对象 班长就是一个对象
  • 类:相似对象的集合,抽象的。是一组相关的属性和行为的集合

    • 描述学生事务
      • 属性:姓名,年龄,性别
      • 行为:学习,吃饭,睡觉
      • 属性:就是该事物的描述信息【成员变量】
      • 行为:就是该事物能够做什么【成员方法】

区别

  1. 类抽象的;对象是具体的;
  2. 类是一个模板,根据这个模板创建出来对象:
  3. 类是引用数据类型,对象是引用变量。
    • String 类   “abc”
    • Person 类 张三

匿名对象

  • 什么是匿名对象
  • 就是没有名字的对象
  • 匿名对象应用场景

    • 调用方法,仅仅只调用一次的时候。
    • 匿名对象可以作为实际参数传递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class switchDemoString {
    public static void main(String[] args) {
    // new Thread()就是没有名字的对象;
    new Thread(){
    public void run(){
    System.out.println("hello world");
    }
    }.start();
    }
    }

类的定义

  • 类名

    • 类名:帕斯卡命名法
    • 多个单词组成,每个单词首字母大写。如Demo,MyDemo
  • 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class 类名{
    //定义属性部分
    属性1的类型属性1
    属性2的类型属性2
    ..
    属性n的类型属性n;

    //定义方法部分
    方法1
    方法2
    ...
    方法m;
    }

创建对象过程

  • A s = new A();
  • 加载A.class文件进内存
  • 在栈内存为s开辟空间
  • 在堆内存为学生对象开辟空间
  • 对学生对象的成员变量进行默认初始化
  • 对学生对象的成员变量进行显示初始化
  • 通过构造方法对学生对象的成员变量赋值
  • 学生对象初始化完毕,把对象地址赋值给s变量

对象的内存结构

  • 对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头的结构

  • HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为”Mark Word”。
  • 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。

如何计算或者获取某个Java对象的大小

  • 获取一个JAVA对象的大小,可以将一个对象进行序列化为二进制的Byte,便可以查看大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//获取一个JAVA对象的大小,可以将一个对象进行序列化为二进制的Byte,便可以查看大小
Integer value = 10;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos ;
try {
oos = new ObjectOutputStream(bos);
oos.writeObject(value);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
// 读出当前对象的二进制流信息
Log.e("打印日志----",bos.size()+"");
//打印日志----: 81

类中的成员

成员变量

  • 在类中直接定义的。
  • 系统自动初始化。

局部变量

  • 在方法或代码块中声明的变量
  • 自己初始化,系统不会自动初始化

成员变量和局部变量区别

  • 作用域
    • 成员变量:类中方法外
    • 局部变量:方法定义中或者方法声明上
  • 内存中的位置
    • 成员变量:在堆中
    • 局部变量:在栈中
  • 生命周期
    • 成员变量:随着对象的创建而存在,随着对象的消失而消失
    • 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
  • 初始值
    • 成员变量:有默认值
    • 局部变量:没有默认值,必须定义,赋值,然后才能使用

Java方法

  • 定义:方法就是完成特定功能的代码块
    • 系统方法,只需要会使用,不需要知道内部的结构
    • 自定义方法:方法的内部实现需要我们来写
    • 方法内部不能再嵌套方法

格式

  • 方法格式

    1
    2
    3
    4
    修饰符 返回值类型 方法名(参数类型 参数名 , ……){
    函数体
    return 返回值
    }
  • 修饰符: 后面介绍

  • 返回类型:所有的数据类型(基本类型:int ,char ,float, long, 引用类型:String)

    • 返回的值的类型与声明的类型必须一致且只能返回一个值。
    • 没有返回值使用void表示
  • 方法名:见名知意或 getXXX/setXXX

  • 参数类型:所有的数据类型,没有参数不用写

  • 参数名: 局部变量名

  • 返回值:就是要返回的结果(这个结果必须和返回类型一致)

  • 方法内 this关键字:表示当前调用方法的对象

方法的参数传递

  • 基本数据类型作为参数

    • 数据不影响
    • 数据在二个栈区,互不影响
  • 引用数据

    • 传的引用,能修改堆内存的数据
  • 示例:

    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
    class Param{
    int value;
    }

    public class Test{
    public void f1(int n ){
    n=22;
    }

    public void f2(Param p ){
    p.value=22;
    }

    public void f3(Param p ){
    // 局部变量p指向了新开辟的堆
    p=new Param();
    p.value=33;
    // 方法结束后p应用消失,堆等待回收
    }

    public static void mian(){
    Test t1 = new Test();
    int m=11;
    t1.f1(m);
    System.out.println(m); //m=11; 基本数据类型不影响
    //-------------------------------------
    Param p1 = new Param();
    t1.f2(p1);
    System.out.println(p1.value); //22 传的引用,修改堆内存的数据
    //-------------------------------------
    t1.f3(p1);
    p1.value=11;
    System.out.println(p1.value); //11 传的引用,但是内部引用指向了新的堆
    }
    }

可变参数

  • 格式: 权限修饰符 返回类型 方法名(数据类型... 名称(数组名))

  • 底层是数组:形参:new int[]{输入的数据,…}

  • 特点:

    • 可以不传参
    • 传一个 或 多个参数
    • 传一个数组
  • 注意:

    • 可变参数只能有一个
    • 多个参数时,可变参数必须放最后
  • 数组参数 和 可变参数 的区别

    • 数组作参数,只能穿参 数组
    • 数组参数 可以有多个
    • 数组参数 参数可以任意位置
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class StudentTestMethod {
    // 定义输出考试学生的人数及姓名的方法
    public void print(String...names) {
    int count = names.length; // 获取总个数
    System.out.println("本次参加考试的有"+count+"人,名单如下:");
    for(int i = 0;i < names.length;i++) {
    System.out.println(names[i]);
    }
    }
    public static void main(String[] args) {
    // TODO Auto-generated method stub
    StudentTestMethod student = new StudentTestMethod();
    student.print("张强","李成","王勇"); // 传入3个值
    student.print("马丽","陈玲");
    }
    }

方法的重载

  • 通常是一个类中, 方法名相同,参数列表不同

  • 作用: 名字方便记忆

  • 返回值不能作为区分方法重载的依据

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //注意要明确 参数列表不同
    public void f(int a ,double b){
    System.out.println("int_ double");
    }

    public void f(double b,int a){
    System.out.println("double_int");
    }


    //参数为引用类型:可以重载,但是:对象.f(null); //编译不能通过
    public void f(String str) {

    }

    public void f(Demo1 demo) {

    }

可变参数的重载

  • 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 重载失败,都是数组
    public void f(int[] arr){

    }

    public void f(int... arr){

    }


    // 可以重载
    public void f(int arr){

    }

    public void f(int... arr){
    }

递归

  • 如果在函数中存在着调用函数本身的情况,这种现象就叫递归。

  • 思想就是,将大问题分解为小问题来求解,然后再讲小问题分解为更小的问题。这样一层一层的分解,直到问题规模被分解的足够小,不用继续分解,可以直接计算结果为止。

  • 递归的一般结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void func()  {
    if(符合边界条件) {
    ///
    return;
    }

    //某种形式的调用
    func();
    }
  • 示例:求阶乘函数

    1
    2
    3
    4
    5
    6
    public int factorial(int n) {
    if (n < =1) {
    return 1;
    }
    return n * factorial(n - 1)
    }

爬楼梯

  • 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。

    1
    2
    3
    4
    5
    6
    输入: 3
    输出: 3
    解释: 有三种方法可以爬到楼顶。
    1. 1 阶 + 1 阶 + 1
    2. 1 阶 + 2
    3. 2 阶 + 1
  • 思路

    本问题其实常规解法可以分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和

    爬上 n-1 阶楼梯的方法数量。因为再爬1阶就能到第n阶
    爬上 n-2 阶楼梯的方法数量,因为再爬2阶就能到第n阶
    所以我们得到公式 dp[n] = dp[n-1] + dp[n-2]
    同时需要初始化 dp[2]=2 和 dp[1]=1
    时间复杂度:O(n)O(n)

  • 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Solution {
    public int climbStairs(int n) {
    int[] stairs = new int[n + 1];
    return climb(n, stairs);
    }

    private int climb(int n, int[] stairs) {
    if (n == 1) return 1;
    if (n == 2) return 2;
    if (stairs[n] > 0) {
    return stairs[n];
    }
    stairs[n] = climb(n - 1, stairs) + climb(n - 2, stairs);
    return stairs[n];
    }
    }