目 录CONTENT

文章目录

Java核心卷(一)面向对象程序设计

Jinty
2023-12-15 / 0 评论 / 0 点赞 / 14 阅读 / 24778 字

封装

封装:将特定的域封装成特定的类,从而划分不同的类

优点:

  1. 便于管理

  2. 只通过特定的方法访问,防止对象域被破坏

  3. 可以改变内部实现,且不影响其他类

  4. 更改器可以执行错误检查

访问器

注意:不要编写返回引用可变对象的访问器

Class Employee{
    private Date hiredate;
    public Date getHireDate(){
        return hireDate;
    } 
}

// 因为这样获取的对象能改变其属性值:
//    exmployee.getHireDate().setTime(123456L);
// Date类有更改器(LocalDate没有),对象可变,这样就破坏了封装性

// 可以返回clone
public Date getHireDate(){
        return (Date)hireDate.clone();
}

构造函数

  1. 构造函数没有返回值

  2. 所有对象都是在中构造,总是伴随new操作一起使用

方法参数

  1. Java中方法都是按值传递参数,即形参为拷贝后的值

  2. 传递对象参数拷贝的是对象的引用,即可修改对象相应属性

  3. 方法不能让对象参数引用一个新的对象,即不能修改对象实参的引用(地址)(对象参数:无法修改内存地址指向,可以修改内存地址中的内容)

  4. 方法不能改变基础数据类型参数(可以使用Holder进行包装)

对象的析构和finalize方法

  1. 可以为任何类添加finalize方法,该方法将在垃圾回收器清除对象前调用(实际使用中不用依赖该方法)

  2. 有个叫System.runFinalizersOnExit(true)可以确保finalize方法被调用(但并不安全,不推荐使用,可以使用替代RunTime.addShutdownHook添加“关闭钩”)

静态方法

  1. 不能访问该类的非静态成员(属性、方法、this引用)

  2. 在静态方法中如何获取当前类名称?(new Object(){}.getclass.getName()

重载(overload)与覆盖(override)

方法签名:方法名称+参数(类型)列表称为方法的签名。

同类中相同方法名称不同方法签名:重载

继承关系类中相同方法名称不同方法签名:覆盖 (多接口实现时的默认方法冲突问题)

在覆盖一个方法时,子类方法不能低于超类方法的可见性

理解方法调用

(1)编译器查看对象的声明类型和方法名称 => 获取所有可能被调用的候选方法 eg:f(String) f(int)

(2)编辑器查看调用方法时提供的参数类型 =>获得需要调用的方法名称和参数类型 eg:f(String)

(3)如果是private、static、final、方法或构造器,编译器可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定(static binding)

当采用动态绑定调用方法时,编译器采用动态方式生成一条调用方法的指令 eg:f(String)

(4)当程序运行时,采用动态调用时,虚拟机一定调用与x引用对象的实际类型最合适的那个类的方法(如果该类定义了方法则直接调用它,否则在其超类中类推寻找)

类访问属性

default:只允许在同一个包下进行访问,其他不能(没在同一个包下的子类也不能,在同一包中的其他类也可直接访问)

protected:只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问

包导入

静态导入

import static java.lang.System.out;

类路径

  1. JAR文件使用ZIP格式组织文件和子目录

  2. Unix与Windows区别:Unix使用':',Windows使用';'分隔类路径中的不同项目,Unix禁止使用'*'

  3. 虚拟机和编译器定位文件不同

  4. 设置类路径:
    (1)-classpath 选项java -classpath /home/classdir:.:/home/archives\archive.jar MyProg
    (2)CLASSPATH 环境变量 (不建议)export CLASSPATH=/home/classdir:.:/home/archives\archive.jar

类设计技巧

  1. 一定要保证数据私有

  2. 一定要对数据初始化

  3. 不要在类中使用过多的基础数据类型(而是新封装一个类)

  4. 不是所有域都需要独立的访问器和更改器

  5. 将职责过多的类进行拆分

  6. 命名规范,体现职责

  7. 优先使用不可变类

super

只是一个指示编译器调用超类方法的特殊关键字,不能理解为引用

super(); //调用超类的构造函数,必须是子类构造器的第一条语句
super.getParm(); ////调用超类的方法

多态

多态:一个对象变量可以指示多个实际类型的现象称为polymorphism

在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)

Java不支持多继承

可以将子类引用赋值给超类变量,不能把超类引用赋值给子类变量

动态绑定重要特性:无需对现存代码修改就可以对程序进行扩展

final

final:确保它们不会在子类中改变语义

如果不希望方法具有虚拟特征可以将其标记为final,

阻止继承:final类和方法,(final类所有方法都自动为final方法,但不包括域)

内联

如果一个方法没有被覆盖并且很短、被频繁调用,那么即时编译器就能够对它进行优化处理,这个过程称为内联,eg: e.getName() 将被替换成e.name

类型转换

只能在继承层次进行类型转换

在将超类转换为子类之前,应该使用instanceof进行检查

一般情况还是少用类型转换和instanceof运算符

抽象类

包含一个或多个抽象方法的类本身必须被声明为抽象类

除抽象方法外,抽象类还可以包含具体数据和方法(建议尽量将通用的域和方法(不管是否为抽象的)放在超类中不管是否为抽象类))

抽象类不能被实例化(可以定义指向子类的抽象类对象)

Object类:所有类的超类

equals方法

在Object类中该方法是判断两个对象是否具有相同引用;

在子类中定义equals方法时,首先调用超类的equals方法

@override
//boolean equals(Employee other){
boolean equals(Object other){
    if(supper.equals(other))  return true; //具有相同引用
    [Classname] otherThisObj = (Employee)other;
    return Objects.equals(this.getId(), other.getId());
}

为防止两个null的比较可以用Objects.equals(Object,Object)方法

java语言规范要求equals方法具有下面特性:

  1. 自反性

  2. 对称性
    如果子类能够拥有自己相等概念,则强制采用getClass进行检测;
    如果超类决定相等概念,可以使用instanceof进行检测,并应该讲方法声明为final

  3. 传递性

  4. 一致性

  5. 对任意非空引用x,x.equals(null) 应该返回false

if(this == other) return true;
if(other == null) return false;
if(getClass() != other.getClass) return false;
// or 所有子类拥有统一的语义:
// if (!(other instanceof [ClassName])) return false;

// 转换为当前类对象
[ClassName] otherThisObj = ([ClassName])  other;

return filed1 == otherThisObj.field1 && ...;

如果重新定义equals方法,那么必须重新定义hashCode方法,且两者的定义必须一致(如果equals返回true,那么两个对象的hashCode应返回相同的值)

以便将对象插入到散列表中

hashCode方法

String 与 StringBuffer中的hashCode方法:

String类中定义了hashCode方法,而StringBuffer中没有定义,其散列码是由Object默认hashCode方法导出的对象存储地址

Objects.haseCode方法,如果对象为null则返回0.

toString方法

数组的toString方法可以使用Arrays.toString进行修正。

包装器对象

对象包装器类为final类,不能定义子类

一旦构造了包装器对象,就不允许修改包装在其中的值。(不能修改方法参数的内容,如果想,可以使用org.omg.COBRA包中定义的持有者类型,包括IntHolder、BooleanHolder等)

public static void holder(IntHolder var) {
    var.value++;

}
public static void main(String[] args) {
    IntHolder var = new IntHolder(12);
    holder(var);
    System.out.println(var.value);
}

自动装箱和拆箱,编译器认可(非虚拟机)

自动装箱规范要求boolean、byte、char<=127,介于-128~127之间的short和int被包装到固定的对象中。

参数数量可变方法

public double max(double... values)

枚举类

在比较两个枚举类型的值时,永远不要使用equals方法,而是直接使用==(不同的类型其实对应着不同的实例);

在枚举类中添加的(私有)构造器和方法;

int ordinal(): 返回枚举在enum声明中的位置,从0开始计数。

int compareTo(E other) : -1-this之前 0:this=other >0: this之后

反射

Class类:运行时类型标识、存储类运行时的信息

Class类是一个泛型类(Class),一个Class对象实际表示的是一种类型,而这种类型未必一定是一个类(如int不是类,int.class是Class类型的对象)

鉴于历史原因,getName方法在应用于数组类型时会返回奇怪的名字:

int[].class.getName() => [I

Double[].class.getName()=>[Ljava.lang.Double;

除非拥有访问权限,Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值(可以使用setAccessible方法修改,操作后记得改回原有的访问属性)

继承的设计与技巧

  1. 将公共操作和域放在超类

  2. 不要使用受保护的域

  3. 使用继承实现“is-a”关系

  4. 除非所有继承的方法都有意义,否则不要使用继承

  5. 在覆盖方法时,不要改变预期的行为

  6. 使用多态,而非类型信息

  7. 不要过度使用反射

接口

在Java中,接口不是类(不能new),而是对类的一组需求描述,类的实现要遵从接口描述的统一格式定义。

接口中的方法自动为public属性(接口中可以不加,但在实现类中实现时需要加上)

接口中的域将被自动设置为public static final

接口不能含义实例域,在SE8前也不能在接口中实现方法

在SE8中允许在接口中增加静态方法

接口中可以定义默认方法实现,默认方法可以调用其他任何方法(默认方法冲突:超类优先、(继承的两个接口方法冲突)子类必须覆盖接口的默认方法)

Comparable接口

任何实现Comparable接口的类都要包含int compareTo(Object other)方法

SE5以改进为泛型类型

Cloneable接口

浅复制:相同引用(默认)

深复制:不同引用

实现Cloneable接口中的T clone(T source)方法

接口与回调 - ActionListener接口

实现ActionListener接口中的void actionPerformed(ActionEvent event)方法

Comparator接口

实现Comparator接口中的int compare(T first, T second)方法

包含了很多方便的静态方法来创建比较器,这些方法可以用于lambda表达式或方法引用。

内部类

定义在另一个类中的类

  • 内部类可以访问该类定义在作用域中的数据,包括私有数据

  • 内部类可以对同一包中的其他类隐藏

  • 想定义一个回调函数而不想编写大量代码时,使用匿名内部类比较方便

内部类的对象总有一个隐式引用- 即指向创建它的外部类对象

OuterClass.this.innerObject

内部类不能有static方法

局部内部类

定义在方法里的类

public void start(){
	class HelloWorld implements ActionListener{
        public void afterPerformed(ActionEvent event){
        	System.out.println("Hello world!");
         }
	}
	ActionListener listener = new HelloWorld();
	Timer t = new Timer(1000, listener);
	t.start();
}

对外部完全隐藏

局部类的方法只能应用final属性的局部变量

public void start(final int beep){
	class HelloWorld implements ActionListener{
        public void afterPerformed(ActionEvent event){
        	System.out.println("Hello world!");
        	System.out.println(beep);
         }
	}
	ActionListener listener = new HelloWorld();
	Timer t = new Timer(1000, listener);
	t.start();
}

匿名内部类

public void start(final int beep){
	ActionListener listener = new ActionListener(){
        public void afterPerformed(ActionEvent event){
        	System.out.println("Hello world!");
        	System.out.println(beep);
         }
	};
	Timer t = new Timer(1000, listener);
	t.start();
}

不能有构造器

实现接口时不能有任何构造参数

方法实现定义在{}内

静态方法中获取当前类类名:new Object(){}.getClass().getEnclosingClass()

静态内部类

内部类不需要引用外围类对象时,可以将内部类声明为static,以便取消产生的引用。

代理

有一个表示接口的Class对象,其确切的类型在编译期无法知道

要想构造一个实现该接口的类,就需要使用newInstance方法或反射找出这个实现类的构造器,

而且由于无法预先定义该实例对象(接口无法实例化),就需要在运行时定义一个新类。

为解决这问题,以前的程序是动态的生成代码文件,然后调用编译器编译后加载结果类文件(无疑很慢)

而代理机制可以更好的解决这一问题。

InvocationHandler接口 - 调用处理器接口

实现接口的 Object invoke(Object proxy, Method method, Object[]  args)

代理对象

生成代理对象: Proxy类的newProxyInstance方法,三个参数:

  • 一个类加载器

  • 一个Class对象数组

  • 一个调用处理器

如何定义处理器?

能够用结果代理对象做些什么?

=> 取决于打算使用代理机制解决什么问题

  • 代理类是在程序运行时创建的,一旦被创建就变成常规类,虚拟机中额其他任何类没有区别

  • 所有的代理类都扩展于Proxy类

  • 所有的代理类都覆盖了Object类中toString、equals、hashCode方法(这些方法仅仅调用了处理器的invoke方法),其他方法如clone、getClass没有被重新定义

  • 对于特定的类加载器和预设的一组接口来说,只能有一个代理类

  • 代理类一定是public和final

  • Proxy.isProxyClass检测一个特定的对象是否表示一个代理类

  • 没有定义代理类的名字,Sun虚拟机中的Proxy类将生成一个以字符串$Proxy开头的类名

使用代理机制实现登录拦截的例子:

//拦截器接口类:
public interface Interceptor {
    /**
     * 执行前
     * @return
     */
    boolean before(Object proxy, Object target, Method method, Object[] args);

    /**
     * 执行环绕
     */
    void around(Object proxy, Object target, Method method, Object[] args);


    /**
     * 执行后
     */
    void after(Object proxy, Object target, Method method, Object[] args);


}

//登录拦截实现类
public class LoginInterceptor implements Interceptor {
    @Override
    public boolean before(Object proxy, Object target, Method method, Object[] args) {
        System.out.println(method.getName() + "\tbefore: do something");
//        if ("doLogin".equals(method.getName())) {
//            User user = (User) args[0];
//            if (null == user) {
//                return false;
//            }
//            if (null == user.getPassword()) {
//                System.out.println("do login filter: password is null");
//                return false;
//            }
//            if (null != user.getLoginName() &&
//                    user.getLoginName().equals("test")) {
//                return false;
//            }
//        }
        return true;
    }

    @Override
    public void around(Object proxy, Object target, Method method, Object[] args) {
        System.out.println(method.getName() + "\taround: do something");
    }

    @Override
    public void after(Object proxy, Object target, Method method, Object[] args) {
        System.out.println(method.getName() + "\tafter: do something");
    }
}

//JDK动态代理-拦截-调用处理器类:
public class InterceptorJdkProxy implements InvocationHandler {
    private Object target;          // 代理目标
    private Class interceptorClass; // 拦截器类

    /**
     * 构造器 - 指定拦截目标和拦截器
     *
     * @param target
     * @param interceptorClass
     */
    public InterceptorJdkProxy(Object target, Class interceptorClass) {
        this.target = target;
        this.interceptorClass = interceptorClass;
    }

    /**
     * 生成代理对象
     * @param target
     * @param interceptorClass
     * @return
     */
    public static Object bind(Object target, Class interceptorClass) {
        return Proxy.newProxyInstance(
                // 一个类加载器 (用于生成代理类名字, )
                target.getClass().getClassLoader(),
                // 一个Class对象数组
                target.getClass().getInterfaces(),
//                new Class[]{},
                // 一个调用处理器
                new InterceptorJdkProxy(target, interceptorClass));
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        /*拦截器未指定 直接执行 不作拦截*/
        if (interceptorClass == null){
            return method.invoke(target, args);
        }

        /* 反射生成拦截器对象*/
        Interceptor interceptor = (Interceptor)interceptorClass.newInstance();

        /*拦截器前置方法*/
        if (interceptor.before(proxy, target,method,args)){
            /*拦截器环绕方法*/
            interceptor.around(proxy, target,method,args);
        }
        /*拦截器后置方法*/
        interceptor.after(proxy, target,method,args);

        return method.invoke(target, args);
    }
}

扩展:JDK代理和Cglib代理

import java.lang.reflect.Method; 
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
public class CglibProxy implements MethodInterceptor
{
    // 根据一个类型产生代理类,此方法不要求一定放在MethodInterceptor中
    public Object CreatProxyedObj(Class<?> clazz)
    {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }
    
    @Override
    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
    {
        // 这里增强
        System.out.println("收钱");
        return arg3.invokeSuper(arg0, arg2);
    } 
}

从代码可以看出,它和jdk动态代理有所不同,对外表现上看CreatProxyedObj,它只需要一个类型clazz就可以产生一个代理对象, 所以说是“类的代理”,且创造的对象通过打印类型发现也是一个新的类型。不同于jdk动态代理,jdk动态代理要求对象必须实现接口(三个参数的第二个参数),cglib对此没有要求。

cglib的原理是这样,它生成一个继承B的类型C(代理类),这个代理类持有一个MethodInterceptor,我们setCallback时传入的。 C重写所有B中的方法(方法名一致),然后在C中,构建名叫“CGLIB”+“$父类方法名$”的方法(下面叫cglib方法,所有非private的方法都会被构建),方法体里只有一句话super.方法名(),可以简单的认为保持了对父类方法的一个引用,方便调用。

这样的话,C中就有了重写方法、cglib方法、父类方法(不可见),还有一个统一的拦截方法(增强方法intercept)。其中重写方法和cglib方法肯定是有映射关系的。

C的重写方法是外界调用的入口(LSP原则),它调用MethodInterceptor的intercept方法,调用时会传递四个参数,第一个参数传递的是this,代表代理类本身,第二个参数标示拦截的方法,第三个参数是入参,第四个参数是cglib方法,intercept方法完成增强后,我们调用cglib方法间接调用父类方法完成整个方法链的调用。

这里有个疑问就是intercept的四个参数,为什么我们使用的是arg3而不是arg1?

    @Override
    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
    {
        System.out.println("收钱");
        
        return arg3.invokeSuper(arg0, arg2);
    }

因为如果我们通过反射 arg1.invoke(arg0, ...)这种方式是无法调用到父类的方法的,子类有方法重写,隐藏了父类的方法,父类的方法已经不可见,如果硬调arg1.invoke(arg0, ...)很明显会死循环。

所以调用的是cglib开头的方法,但是,我们使用arg3也不是简单的invoke,而是用的invokeSuper方法,这是因为cglib采用了fastclass机制,不仅巧妙的避开了调不到父类方法的问题,还加速了方法的调用

fastclass基本原理是,给每个方法编号,通过编号找到方法执行避免了通过反射调用。

对比JDK动态代理,cglib依然需要一个第三者分发请求,只不过jdk动态代理分发给了目标对象,cglib最终分发给了自己,通过给method编号完成调用。cglib是继承的极致发挥,本身还是很简单的,只是fastclass需要另行理解。

0

评论区