Java 面试复习_1

本文探讨了面向对象编程的三大特性:封装、继承和多态,强调了封装的益处和继承的潜在问题。文章通过实例解释了多态的重要性,并比较了继承与组合的差异。此外,还涉及了Java中的final、finally、finalize的区别,基本类型与包装类的差异,重载和重写的概念,接口与抽象类的区别,反射与注解的用途,以及HTTP请求方式和Restful风格。最后讨论了session与cookie在会话管理中的应用及其在分布式环境下的挑战。
摘要由CSDN通过智能技术生成


2019-5-17
作者:水不要鱼

(注:能力有限,如有说错,请指正!)

  • OOP 思想

  1. 封装 (基础)
    • 把一些内部实现隐藏,只公开一部分“想公开”的内容。
    • 外界只需要使用这部分公开的内容即可,不需要关心内部实现
  2. 继承 (实现多态的一种手段)
    • 子承父业,没啥好说的
  3. 多态 (OOP 的精髓)
    • 一个标准可以有多个实现,而为了解决代码之间的耦合性,我们不会在代码中直接引入具体的标准实现。

扩展:OOP 编程

这是一个非常有价值有深度的问题,我们可以来讨论一下。

  • OOP 强调封装,也就是把内部实现隐藏,但是内部不一定包含数据。

    我们来看一个例子:

    public class Entity {
      private static final String PREFIX = "Hello";
      private static final String SUFFIX = "World";
      public String getName() {
        return PREFIX + SUFFIX;
      }
    }
    

    由于多了一层方法的封装,所以内部数据更改了,外界可以不用去感知,
    甚至还傻傻的以为内部真的有 name 属性。
    这就是封装的好处。

  • OOP 中有继承,但是继承一直被人诟病,甚至于在 Golang 中完全被阉割,取而代之的是组合。
    继承不是说不好,而是会将整个类的体系变得非常庞大而且难于理解,简单的系统还好,如果是成百上千个类的继承关系,
    那就等着哭吧,维护的工作将变得非常困难,而且对于新入职的员工来说难于理解。

    另外,我个人觉得继承不好还有一个原因就是在于意义层面。举个例子,小明的父亲可以借钱给别人。

    class XiaoMing extends XiaoMingFather {}
    class XiaoMingFather {
      public double borrow(double numberOfMoney) {
        // 你说要借多少,他就借多少,小明父亲就是这么豪
        return numberOfMoney;
      }
    }
    

    当我们要向小明借钱的时候,相当于调用 xiaoMing.borrow(666.666),
    然而,这个方法并不属于小明,它是小明父亲的方法,我们可以看到小明的类里面并没有直接声明这个方法,
    所以这相当于我们直接向小明父亲借钱,而我们根本不认识小明父亲,这不符合逻辑。

    如果改用组合呢?

    class XiaoMing {
      private XiaoMingFather father = new XiaoMingFather();
      public double borrow(double numberOfMoney) {
        // 你说要借多少,他就借多少,有其父必有其子
        return father.borrow(numberOfMoney);
      }
    }
    class XiaoMingFather {
      public double borrow(double numberOfMoney) {
        // 你说要借多少,我就借多少,小明父亲就是这么豪
        return numberOfMoney;
      }
    }
    

    很明显可以看到代码不一样了,我们在小明内部镶嵌了一个小明父亲,而我们向小明借钱的时候,
    是直接调用的小明的方法,而不是小明父亲的方法,虽然在小明内部还是调用了小明父亲的方法,
    但是对于外界我们是不知情的,我们只认识小明,也只会跟小明借钱,
    至于小明找谁拿的钱,我们不在乎,我们只在乎能不能拿得到钱。。。

    可以看得出,组合相比继承更具逻辑性,而且我们可以看到小明和小明父亲没有直接的联系,就是说小明内部可以随时更换借钱的人,
    只需要很简单的换个父亲即可(只要小明愿意),如果是继承,这就不好办了,由于我们直接调用的是小明父亲的方法,
    所以如果小明要换一个父亲,调用方就需要随着改变。

  • 与其说多态是 OOP 的精髓,不如说多态是面向接口的精髓。当我们在 Java 中使用多态的时候,
    要不就是使用继承,要不就是使用接口。

    我们来看一个例子:

    interface Walkable {
      void walk();   
    }
    class Human implements Walkable {
      public void walk() {
        System.out.println("我是人类,直立行走!");
      }
    }
    class Dog implements Walkable {
      public void walk() {
        System.out.println("我是狗狗,四条腿走!");
      }
    }
    class Entity {
      public int walk(Walkable walkable) {
        return walkable.walk();
      }
    }
    

    当我们调用 Entity::walk 方法时,你可能是这样的:walk(new Human()),也有可能是这样的:
    walk(new Dog())。不管哪一种方式,执行结果都会不一样,因为这就是多态的价值,
    同样的调用会带来不一样(多种状态)的结果。

    那我们反过来想,对于 Entity 的 walk 方法来说,它不用去关心具体实现,也没必要去关心,
    因为不管是哪一种结果,对它都没有差别,因为都是走。

    多态对于很多代码设计都有很大的价值,我知识有限,就点到为止吧。

  • final, finally, finalize 的区别

  1. final: 修饰类的关键字
  2. finally: 异常处理块
  3. finalize: 这是 Object 的一个方法
    public class Test {
       @Override
       protected void finalize() throws Throwable {
           super.finalize();
              
           // 不建议重写这个方法!!!
           // do something...
           // 在垃圾收集器执行的时候会调用被回收对象的此方法
           // 但是,需要注意,这个方法并不保证一定会被执行
       }
    }
    
  • 基本类型和包装类

  1. 基本类型不可以有空值
  2. 包装类型可以有空值

扩展:包装类的意义

基本类型在对大部分需求已经足够了,而且相比包装类更省内存,但是基本类型没有办法表达空值。
比如,一个同学缺考了,要怎么记录这个分数?

可以用 -1,这是一种办法,但是语义不够清楚,如果不说,你可能得猜猜这代表什么意思,
另外,如果对一个需求来说整个范围都是有意义的呢,那用什么来表示空值?这时候包装类就可以很好的表示了,
毕竟 null 不就是空了吗。最主要的是,一些集合类,比如 List 没办法用基本类型。。。这是一个硬伤。

  • 重载和重写(覆盖)

  1. 重载是一个类中多个方法名字相同,但是参数列表不相同
    • 名字一样,参数列表不同
    • 返回值呢?返回值不能用来区分方法,也就是说两个方法,只有返回值不一样的话,
      这两个是属于同一个方法
  2. 重写(覆盖)是子类写了一个父类中已经存在的方法
    • 注意:子类和父类,这就存在一层上下级的关系了,这个关系除了继承,还可以是接口的实现
    • 由于 Java 中默认多态,所以使用父类引用(这个父类引用指向的是子类对象)时,可以调用到子类的实现
  • 接口和抽象类

  1. 抽象类,顾名思义是一个类
  2. 接口,是一种协议或者标准

扩展:两者的区别

在 JDK7 以前的版本,两者的区别还比较大,比如接口不能有默认实现,
但是在 JDK8 之后,情况就变了,JDK 官方似乎一直在模糊两者的边界,让接口也可以有默认实现,
只需要使用 default 关键字:

interface Coder {
    default void code() {
       System.out.println("coding...");
    }
}

在很多的框架设计中,一般都会有这样的设计,首先使用接口,比如上面的 Coder 接口,
然后创建一个抽象类,比如 AbstractCoder,然后再有具体的是实现类,比如 JavaCoder。

接口一般不会设计的太复杂,通常只有一个方法,这样可以保证接口的简洁,在维护上也方便,用户实现也方便很多。

  • 反射和注解

  1. 利用反射可以拿到一个类运行时的数据
  2. 借助反射和注解可以实现一些非常灵活的操作,而且低耦合的代码

扩展:反射的性能

虽然反射可以做很多灵活的操作,但这是有代价的 ———— 性能。
我们来看看使用反射之后性能差了多少:

class ReflectTest {
    private String value = null;
    
    public static void main(String[] args) throws Exception {
    
        ReflectTest test = new ReflectTest();
    
        double beginTime = System.currentTimeMillis();
        withoutReflect(test); // 380 ms 左右
        double endTime = System.currentTimeMillis();
        System.out.println("withoutReflect: " + (endTime - beginTime) + " ms.");
    
        beginTime = System.currentTimeMillis();
        withReflect1(test); // 1600 ms 左右
        endTime = System.currentTimeMillis();
        System.out.println("withReflect: " + (endTime - beginTime) + " ms.");
      
        beginTime = System.currentTimeMillis();
        withReflect2(test); // 390 ms 左右
        endTime = System.currentTimeMillis();
        System.out.println("withReflect: " + (endTime - beginTime) + " ms.");
   }
    
   // 不使用反射
   private static void withoutReflect(ReflectTest test) {
       for (int i = 0; i < 10000000; i++) {
           test.value = String.valueOf(i);
       }
   }
    
      // 使用反射
   private static void withReflect1(ReflectTest test) throws Exception {
       for (int i = 0; i < 10000000; i++) {
           Field field = ReflectTest.class.getDeclaredField("value");
           field.setAccessible(true);
           field.set(test, String.valueOf(i));
       }
   }
    
   // 使用反射
   private static void withReflect2(ReflectTest test) throws Exception {
       Field field = ReflectTest.class.getDeclaredField("value");
       field.setAccessible(true);
       for (int i = 0; i < 10000000; i++) {
           field.set(test, String.valueOf(i));
       }
   }
}

很明显,使用反射之后性能有所下降,执行时间变成了原来的四倍,
但是对比 withReflect1 和 withReflect2 可以发现,获取反射值才是最耗时的,
也就是说,如果是使用反射修改值,提前获取到反射数据的话,性能下降很少很少。

总的来说,使用反射之后,性能有所下降,但是这个差距要在数量级非常庞大才能体现,
如果量级只有几十万次执行,这个下降就不算太大的影响,而且做了预处理之后,性能几乎没有下降,
相比反射带来的便利,显然性能下降的那一点点也就可以接受了。

  • HTTP 请求方式

  1. 目前常用的有 GET/POST/PUT/DELETE/HEAD/OPTIONS
  2. 每一种方式在使用的时候都应该有对应的意义

扩展:RestFul 风格

HTTP 中有多种请求方式,每一种其实都是有用的,尤其是在语义上,会有很大的区别。
而 RestFul 也仅仅是一种风格,并不是具体的标准或者要求,它就是建议请求使用符合语义的请求方法。

  • GET 请求,用于获取数据,有些浏览器会对这个请求方式的请求进行缓存,
    所以如果是一个修改数据的请求使用了 GET 请求,就有可能导致这个请求被缓存,
    导致下一次请求失效,但是这个情况很少很少,除了会被缓存之外,由于 GET 请求使用 url 来传递参数,
    所以参数会被直接暴露在 url 中,也就有了它不安全的说法,还有部分浏览器会对 url 长度做限制,这时候 GET 请求中的数据就会被限制。

  • POST 请求,用于保存数据,和 GET 不同的是,请求数据会被隐藏到请求体中,也就是人们所说的“安全”,
    但是这还是明文传输的,如果真的需要加密,应该考虑的是 HTTPS 而不是将 GET 更换为 POST,
    你甚至可以对数据进行对称或者非对称加密然后再传输

  • PUT 请求,用于修改数据,这个请求方式在 SpringMVC 中需要配置一个参数才能正常使用,
    具体参考 SpringMVC 的用法,这里不多说

  • DELETE 请求,用于删除数据

  • session 与 cookie

  1. 某种程度上说,这两个毛关系都没有,Session 是服务端对会话的标准或者说接口,
    比如 Java 中的 HttpSession 就是这种标准的产物,而 Cookie 是浏览器的一种存储载体,
    和 localStorage 和 sessionStorage 是相同功能的东西,并不是用来作会话的,
    只是说,你可以将会话信息存在 Cookie 中,以达到会话的功能,而这个功能,
    你使用一个 js 变量就可以达到,甚至还更方便更简单,不过很多人把他们混为一体来谈,
    下面就是以混为一体来谈为前提
  2. cookie 数据存放在客户的浏览器上,session 数据放在服务器上
  3. cookie 不是很安全,别人可以分析存放在本地的 cookie 并进行 cookie 欺骗,
    考虑到安全应当使用 session。
  4. session 会在一定时间内保存在服务器上,也就是有存活周期。
  5. 单个 cookie 保存的数据不能超过 4KB,而且很多浏览器对 Cookie 的存储数量有限制。
  6. 客户端是可以关闭 Cookie 的

扩展:分布式 session 问题

当一个系统拆分到多台服务器之后,就存在多台服务器之间的会话共享问题,
比如现在有 3 台服务器,ABC,我在 A 服务器上登录了,B 服务器如何感知我已经登录了就是分布式系统需要解决的一个题,
这就是分布式 session 问题,解决办法其实有很多,下面列举几种来聊聊

  1. Session 复制:将 A 服务器上的会话信息复制到 BC 服务器上,这样 BC 服务器也就有了我的登录信息,
    这样做就多了一次同步,在请求数很多的时候会占用很大的网络带宽,因为 A 服务器一直在给 BC 服务器发送当前的会话信息。
    而且,同步就会有延迟,除非你在 A 服务器上等待 BC 服务器同步完成才返回给客户,否则就会出现短暂的不同步问题,
    比如我在 A 登录了,在 A 上的会话信息还没同步到 BC 上之前,我访问 BC 服务器都会是处于未登录状态
  2. Session 粘滞:将用户和某台服务器进行绑定,也就是这个用户所有的请求都交给这台服务器处理,这样即使在 BC 上没有
    登录信息也没关系,因为根本不会交给它们处理。但是这样的话就会把应用服务器变成是有状态的,对后期扩展不方便,除此之外,如果 A 服务器绑定的用户比 BC 服务器上的用户要活跃的多,就会导致 A 服务器的负载很高,而 BC 都在空闲
  3. Session 集中管理:将会话信息统一保存到一台独立的服务器上,比如将会话信息保存在 Redis 中,
    所有的Web服务器都从这个 Redis 中存取对应的 Session,实现 Session 共享。你甚至可以将数据持久化保存在数据库或者文件系统中,不过由于引入了独立的服务器,开发和维护当然就更多事情要处理了,不过这个代价是值得的
  4. 基于 Cookie/sessionStorage 管理:将会话信息保存在浏览器本地或者内存中,每一次发请求都携带会话信息(比如将信息保存在请求头中)。这种方式极其不安全,因为请求是可以被伪造的,而且如果信息太多还容易造成 HTTP 请求头太庞大和信息泄露。

上面大概聊了几种实现分布式 session 的思路,最经常用也是最推荐的是基于 Cookie/sessionStorage 管理的
Session 集中管理,也就是上面 3,4 点结合起来用,在 Cookie/sessionStorage 中存的不是用户信息,
而是用户本次登录的凭证,这个凭证由服务器生成并保存在前台,每一次请求都会带上凭证,这样服务器就可以检验并获取到会话信息,同样的,这个凭证也会涉及到伪造的问题,所以一般都会使用特定的加密算法对用户特定信息做指纹处理,生成特殊的用户登录凭证,以防止伪造,比如简单使用用户某些信息作 MD5 加密,又或者是使用 JWT 生成

  • equals 和 ==
  1. == 是 Java 的一个操作符,equals 是 Object 类中的一个方法
  2. equals 默认比较的就是地址,所以对于引用类型来说,和 == 作用是一样的
  3. 通常都会重写这个方法用于比较具体的内容
class Entity {
    private String name = null;
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        // 这里使用 Class 来比较而不是用 instanceof 是因为:
        // instanceof 会把子类当成父类,也就是 "str" instanceof Object ,将返回 true
        if (this.getClass() == o.getClass()) return name.equals((Entity(o)).name);
        return false;
    }
}

今天就到这里!晚安!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值