面向高级对象开发

2 篇文章 1 订阅

面向高级对象开发

Maven项目

1. 项目结构

|—src 源码
|—|---main 主程序
|—|---|—java java源文件
|—|---|—resources 框架或其他工具的配置文件
|—|---test 测试程序
|—|---|—java java测试的源文件
|—|---|—resources 测试的配置文件
|—pom.xml maven工程的核心配置文件
pom.xml:Project Object Model项目对象模型,maven的核心配置文件,与构建过程相关的一切设置都在这个文件中进行配置。

2. maven命令

mvn compile 编译源代码
mvn test-compile 编译测试代码
mvn test 运行测试
mvn site 产生site
mvn package 编译打包
mvn install 在本地Repository中安装jar
mvn clean 清除产生的项目

3. maven的依赖特性

  1. 常见的几种scope
    scope
  2. 依赖范围影响传递性依赖
    依赖范围影响传递性依赖

4. maven的聚合和继承

  • 聚合
    聚合
  • 继承
    继承

JUnit

白盒测试:通过程序的源代码进行测试而不适用用户界面。
黑盒测试:又称为功能测试、数据驱动测试或基于规格说明的测试,是通过使用整个软件或某种软件功能来严格地测试, 而并没有通过检查程序的源代码或者很清楚地了解该软件的源代码程序具体是怎样设计的。测试人员通过输入他们的数据然后看输出的结果从而了解软件怎样工作。
术语:

  • 测试环境:设置运行测试所需要的数据
  • 单元测试:用于测试一个类
  • 测试组件:测试用例的集合
  • 测试运行器:执行测试用例和提交报告

编写一个测试用例

  1. import Junit
    import junit
  2. 初始化变量
    @Before
    在before中放一些对性能损耗比较大的内容。且可以做一些处理工作,使得测试用例在进行前后对环境不产生任何改变。
    @After
  3. 测试方法
    @Test
  4. assertEquals(expected, actual)
    这个函数中使用到了equals,所以在比较一些不是字符串的对象的时候。需要自己定义equals。
    equals
  5. 关于assert的其他方法关于assert的其他方法
    assert 1
    assert 2

测试的其他框架

  1. hamcrest框架
    hamcrest可以用来增强JUnit的assert功能。要使用JUnit中的assertThat来进行断言,第一个参数表示实际值,第二个参数表示hamcrest的表达式。
    Hamcrest是一个书写匹配器对象时允许直接定义匹配规则的框架。有大量的匹配器是侵入式的,例如UI验证或者数据过滤, 但是匹配对象在书写灵活的测试最常用。
    例如:
    hamcrest 1
    hamcrest 2

  2. cobertura框架,用于得到覆盖率报告
    cobertura是完成这项任务的一个免费 GPL 工具。Cobertura 通过用额外的语句记录在执行测试包时, 哪些行被测试到、哪些行没有被测试到,通过这种方式来度量字节码,以便对测试进行监视。然后它生成一个 HTML 或者 XML 格式的报告,指出代码中的哪些包、哪些类、哪些方法和哪些行没有测试到。可以针对这些特定的区域编写更多的测试代码,以发现所有隐藏的 bug。

  3. Stub和Mock
    Stub & Mock

  4. DbUnit
    Stub和Mock虽然可以模拟单元测试,但是其中的一些sql语句还是没有办法准确地测试到,因此使用DbUnit。
    Dbunit是一个基于junit扩展的数据库测试框架。它通过使用用户自定义的数据集以及相关操作使数据库处于一种可知的状态,从而使得测试自动化、可重复和相对独立。

  5. EasyMock
    EasyMock 使用动态代理实现模拟对象创建。可以用来对一些未实现的关联对象的类进行测试。
    EasyMock

代码规范

1. 类、接口

  • 作者和版本:@author, @version
  • 类描述
  • 线程安全
  • 修订历史:(change history)
    类、接口注释

2. 方法块注释

  • 方法描述
  • 输入、输出,包括参数和返回值:@param, @return
  • exception,assumptions,prerequisites and/or limitations:@throws, @see
    方法块注释

3. Declaration Comment

  • 注释阐明静态、实例范围的用途和目的
  • document-style comment that will appear in java doc
    Declaration Comment

4. Implementation Comment

Implementation Comment

5. Source Header Block

  • Copyright Notice
    Source Header Block

CheckStyle

CheckStyle

创建和销毁对象

构造方法 vs. 静态工厂方法

构造方法:

Fragment fragment = new MyFragment();
// or
Date date = new Date();

静态工厂方法:

Fragment fragment = MyFragment.newIntance();
// or 
Calendar calendar = Calendar.getInstance();
// or 
Integer number = Integer.valueOf("3");

静态工厂方法的优点

  1. 静态工厂方法有名称
    由于语言的特性,Java 的构造函数都是跟类名一样的。这导致的一个问题是构造函数的名称不够灵活,经常不能准确地描述返回值,在有多个重载的构造函数时尤甚,如果参数类型、数目又比较相似的话,那更是很容易出错。
    而如果使用静态工厂方法,就可以给方法起更多有意义的名字,比如前面的 valueOf、newInstance、getInstance 等,对于代码的编写和阅读都能够更清晰。
  2. 不用每次被调用时都创建新对象
    使用构造器,每次都会产生一个新的对象。而静态工厂方法,可以重复地返回预先创建好的对象。
    下面Boolean就是一个非常好的例子,TRUE和FALSE两个变量都是预先创建好的,而且都是不可变的final对象,谁需要用到了,就给它返回过去,也不用担心被修改了。
public final class Boolean implements java.io.Serializable,
                                      Comparable<Boolean> {
    /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code true}.
     */
    public static final Boolean TRUE = new Boolean(true);
 
    /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code false}.
     */
    public static final Boolean FALSE = new Boolean(false);
 
    ...   
}
  1. 可以返回原返回类型的任何子类型的对象
    使用构造器,你只能返回一种类型的对象;而使用静态工厂方法,你可以根据需要,返回原返回类型的任何子类型的对象。
    以EnumSet的noneof方法为例:
    /**
     * Creates an empty enum set with the specified element type.
     *
     * @param elementType the class object of the element type for this enum set
     * @throws NullPointerException if <tt>elementType</tt> is null
     */
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");
 
        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }
  1. 在创建带泛型的实例时,能使代码变得简洁
// Parameterized type instances
Map<String, List<String>> m = new HashMap<String, List<String>>();

vs.

// Static factory alternative
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
// Now, client code looks like this
// Compiler does type inference!
Map<String, List<String>> m = HashMap.newInstance();

静态工厂方法的缺点

  1. 公有的静态方法所返回的非公有类不能被实例化
    在使用静态工厂方法的时候,是private
  2. 因为有一些惯用名称,所以在文档中难以查找

遇到多个构造器参数时要考虑用构造器

详解

  1. 使用可伸缩构造函数模式:只提供一个只所需参数的构造函数,另一个只有一个可选参数,第三个有两个可选参数,等等。最终在构造函数中包含所有可选参数。
  2. Javabean模式:调用一个无参数的构造函数来创建对象,然后调用setter方法来设置每个必需的参数和可选参数。
  3. Builder模式:客户端调用构造方法(或静态工厂),使用所有必需的参数,并获得一个builder对象。
// Builder Pattern 构造模式
public class NutritionFacts {
    private final int servingSize;   
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
 
 	// 静态成员类
    public static class Builder {
        // Required parameters  必须填入的参数
        private final int servingSize;
        private final int servings;
 
        // Optional parameters - initialized to default values
        // 可以选填的参数,有初始值的设置
        private int calories      = 0;
        private int fat           = 0;
        private int sodium        = 0;
        private int carbohydrate  = 0;
 
 		// Builder的构造方法,只使用了必需的参数
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }
 
 		// 客户端调用builder对象的setter相似方法来设置每个可选择的参数
        public Builder calories(int val) { 
            calories = val;      
            return this;
        }
 
        public Builder fat(int val) { 
           fat = val;           
           return this;
        }
 
        public Builder sodium(int val) { 
           sodium = val;        
           return this; 
        }
 
        public Builder carbohydrate(int val) { 
           carbohydrate = val;  
           return this; 
        }
 
 		// 最后,客户端调用一个无参的builder方法来生成对象,该对象通常是不可变的
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
 
    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

// 具体调用使用
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

Builder模式的缺点

单例模式

单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:

  • 单例类只能有一个实例
  • 单例类必需自己创建自己的唯一实例
  • 单例类必须给所有其他对象提供这一实例
// Option 1: public final field
public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	private Elvis() {...}
}
// Option 2: static factory method
public class Elvis {
	private static final Elvis INSTANCE = new Elvis();
	private Elvis() {...}
	public static Elvis getInstance() { return INSTANCE; }
}
// Option 3: Enum type – now the preferred approach
// 用枚举实现单例模式
public enum Elvis {
	INSTANCE;
	public void leaveTheBuilding() { ... }
}

枚举的实现:

  • 更简洁
  • 无偿地体用了序列化机制
  • 在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化

通过私有构造器强化不可实例的能力

有时候需要编写只包含静态方法和静态域的类,其一般作为工具类使用,这样的类不需要被实例化。然而在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器(dafault constructor),所以常常可以看到一些被无意识地实例化的类。
这就需要强化此类不可实例化的能力,由于只有当类不包含显式的构造器时,编译器才会生成缺省的构造器,因此我们只需要将这个类包含私有构造器,它就不能被实例化了,如下所示会 Exception in thread “main” java.lang.AssertionError 。

public class Student {
    private Student() {
        throw new AssertionError();
    }
    public static void main(String[] args) {
        new Student();
    }
}

由于显式的构造器是私有的,所以不能在类的外部访问它;其中AssertionError()不是必须的,但是他可以避免不小心在 Student 类的内部调用构造器,从而保证了 Student 类在任何情况下都不会被实例化。
通过私有构造器强化不可实例的能力

避免创建不必要的对象

避免创建不必要的对象
代码示例
代码示例2

消除过期的对象引用(过期引用:永远不会再被解除的引用)

下面代码中栈的实现中包括了过期的对象引用。

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
 
    public Stack(){
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
 
    public void push(Object e){
        ensureCapacity();
        elements[size ++] = e;
    }
 
    public Object pop(){
        if (size == 0){
            throw new EmptyStackException();
        }
        return elements[--size];
    }
 
    private void ensureCapacity(){
        if (elements.length == size){
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

实际上,这段程序中并没有很明显的错误。无论如何测试,它都会成功地运行通过每一项测试,但这个程序中隐藏着一个问题。不严格地讲,这段程序有一个”内存泄漏“, 随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄露会导致磁盘交换,甚至程序失败,但这种情况比较少见。
那么,程序中哪里发生了内存泄漏呢?实际上,当栈先增长后收缩时,即使我们执行push后再pop,这时候弹出来的对象将不会被当作垃圾回收,即便使用栈的程序不会再引用这些对象,也不会被回收。因为栈内部维护着这些对象的过期引用。
过期引用:指的是永远不会再被解除的引用。
过期引用的详解
过期引用的处理

避免使用终结方法

基本概念:
所谓的终结方法其实是指finalize()。终结方法finalizer通常是不可预测的,也是很危险的。一般情况下是不必要的。使用终结方法会导致行为不稳定,降低性能,以及可移植性问题。根据经验,应避免使用终结方法。

SVN(Subversion)

SVN是一种集中式的版本管理,有客户端和用户端之分。

SVN和Git的对比

最核心的区别:
SVN是集中式管理的版本控制器,而Git是分布式管理的版本控制器。
SVN vs. Git
SVN只有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。Git每一个终端都是一个仓库,客户端并不只提取最新版本的文件快照,而是把原始的代码仓库完整地镜像下来。每一次的提取操作,实际上都是一次对代码仓库的完整备份。
在这里插入图片描述

SVN命令

  • svn checkout // 迁出配置库内容
  • svn delete // 删除文件
  • svn export // 导出(导出一个干净的不带.svn文件夹的目录树)
  • svn add // 告诉服务器要添加的文件
  • svn commit // 将add的文件提交上去
  • svn lock // 加锁文件信息

update和commit的区别

在这里插入图片描述

Git

本地版本库基本操作

在这里插入图片描述

  1. 创建版本库(repository)
mkdir file_test
cd file_test
git init
  1. 把文件添加到版本库
git add test.txt
git commit -m "add test.txt"
  1. 查看仓库当前状态
git status
# On branch master
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: readme.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
  1. 比较文件修改变化
    修改test.txt,然后
git diff test.txt
  1. 提交修改
git add test.txt
git commit -m "test diff"
  1. 查看工作区、暂存区状态
git status
  1. 查看历史修改记录
git log
  1. 回退到上一个版本
git reset --hard HEAD^

在 Git 中,用 HEAD 表示当前版本
9. 回到一个指定的版本

git reset --hard 3628164 (3628164 是 commit id)
  1. 查看所有命令历史
git reflog
  1. 工作区、版本库和暂存区
    在这里插入图片描述
    把文件往 Git 版本库里添加的时候,是分两步执行的:
    第一步是用“git add”把文件添加进去,实际上就是把文件修改添加到暂存区;
    第二步是用“git commit”提交更改,实际上就是把暂存区的所有内容提交到当前分支。

练习题

  1. 第一次修改 -> git add -> 第二次修改 -> git commit. 问:此时哪些修改已被提交到版本库?执行 git status 会是什么结果?
  2. 撤销修改(还没提交到暂存区,撤销工作区即可)
git checkout test.txt

(git checkout 其实是用版本库里的版本替换工作区的
版本,无论工作区是修改还是删除,都可以“一键还原”。)
3. 撤销修改(已提交到暂存区)
(1)使用 git reset 把暂存区的修改回退到工作区

git reset HEAD test.txt

(2)然后再丢弃工作区的修改

git checkout test.txt
  1. 删除文件
git rm test.txt
git commit -m "remove test.txt"

泛型

泛型详解

1. 为什么需要泛型?

public class GenericTest {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // 1
            System.out.println("name:" + name);
        }
    }
}

定义了一个List类型的集合,先向其中加入了两个字符串类型的值,随后加入一个Integer类型的值。这是完全允许的,因为此时list默认的类型为Object类型。在之后的循环中,由于忘记了之前在list中也加入了Integer类型的值或其他编码原因,很容易出现类似于//1中的错误。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。

在如上的编码过程中,我们发现主要存在两个问题:
1.当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。
2.因此,//1处取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“java.lang.ClassCastException”异常。

2. 什么是泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

public class GenericTest {

    public static void main(String[] args) {
        /*
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);
        */

        List<String> list = new ArrayList<String>();
        list.add("qqyumidi");
        list.add("corn");
        //list.add(100);                         // 1  提示编译错误

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i);           // 2
            System.out.println("name:" + name);
        }
    }
}

采用泛型写法后,在//1处想加入一个Integer类型的对象时会出现编译错误,通过List,直接限定了list集合中只能含有String类型的元素,从而在//2处无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。

结合上面的泛型定义,我们知道在List中,String是类型实参,也就是说,相应的List接口中肯定含有类型形参。且get()方法的返回结果也直接是此形参类型(也就是对应的传入的类型实参)。

ArrayList作为List接口的实现类,其定义形式是:

public class ArrayList<E> extends AbstractList<E> 
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return ArrayList.this.elementData(offset + index);
    }
    
    //...省略掉其他具体的定义过程
}

由此,我们从源代码角度明白了为什么//1处加入Integer类型对象编译错误,且//2处get()到的类型直接就是String类型了。
结合上面的泛型定义,我们知道在List中,String是类型实参,也就是说,相应的List接口中肯定含有类型形参。且get()方法的返回结果也直接是此形参类型(也就是对应的传入的类型实参)。下面就来看看List接口的的具体定义:

public interface List<E> extends Collection<E> {

    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean addAll(int index, Collection<? extends E> c);

    boolean removeAll(Collection<?> c);

    boolean retainAll(Collection<?> c);

    void clear();

    boolean equals(Object o);

    int hashCode();

    E get(int index);

    E set(int index, E element);

    void add(int index, E element);

    E remove(int index);

    int indexOf(Object o);

    int lastIndexOf(Object o);

    ListIterator<E> listIterator();

    ListIterator<E> listIterator(int index);

    List<E> subList(int fromIndex, int toIndex);
}

我们可以看到,在List接口中采用泛型化定义之后,中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。

3. PECS法则

PECS法则:生产者(Producer)使用extends,消费者(Consumer)使用super
1、生产者
如果你需要一个提供E类型元素的集合,使用泛型通配符<? extends E>。它好比一个生产者,可以提供数据。
2、消费者
如果你需要一个只能装入E类型元素的集合,使用泛型通配符<? super E>。它好比一个消费者,可以消费你提供的数据。
3、既是生产者也是消费者
既要存储又要读取,那就别使用泛型通配符。

4. 自定义泛型接口、泛型类和泛型方法

从上面的内容中,大家已经明白了泛型的具体运作过程。也知道了接口、类和方法也都可以使用泛型去定义,以及相应的使用。是的,在具体使用时,可以分为泛型接口、泛型类和泛型方法。

自定义泛型接口、泛型类和泛型方法与上述Java源码中的List、ArrayList类似。如下,我们看一个最简单的泛型类和方法定义:

public class GenericTest {
    public static void main(String[] args) {
        Box<String> name = new Box<String>("corn");
        System.out.println("name:" + name.getData());
    }
}

class Box<T> {
    private T data;
    public Box() {}

    public Box(T data) { this.data = data; }

    public T getData() { return data; }
}

在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。
究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

4. 类型通配符

  1. 类型通配符
    类型通配符:一个在逻辑上可以用来表示同时是Box和Box的父类的一个引用类型。

例子1:

public class GenericTest {

    public static void main(String[] args) {
        Box<Number> name = new Box<Number>(99);
        Box<Integer> age = new Box<Integer>(712);

        getData(name);
        
        //The method getData(Box<Number>) in the type GenericTest is 
        //not applicable for the arguments (Box<Integer>)
        getData(age);   // 1
    }
    
    public static void getData(Box<Number> data){
        System.out.println("data :" + data.getData());
    }

}

在代码//1处出现了错误提示信息:The method getData(Box) in the t ype GenericTest is not applicable for the arguments (Box)。显然,通过提示信息,我们知道Box在逻辑上不能视为Box的父类。
类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box<?>在逻辑上是Box、Box…等所有Box<具体类型实参>的父类。由此,我们依然可以定义泛型方法,来完成此类需求。
例子2:

public class GenericTest {

    public static void main(String[] args) {
        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);

        getData(name);
        getData(age);
        getData(number);
    }

    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }

}
  1. 类型通配符上限
    在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限
public class GenericTest {

    public static void main(String[] args) {

        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);

        getData(name);
        getData(age);
        getData(number);
        
        //getUpperNumberData(name); // 1
        getUpperNumberData(age);    // 2
        getUpperNumberData(number); // 3
    }

    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }
    
    public static void getUpperNumberData(Box<? extends Number> data){
        System.out.println("data :" + data.getData());
    }
}

显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。

  1. 类型通配符下限
    只能是Number类及其父类,Number是下限,就是Box<? super Number>。

Annotation

在这里插入图片描述

Methods Common to All Objects

《Effective Java》学习笔记10 Obey the general contract when overriding equals

1. 覆盖equals方法时应遵守通用规约

不需要覆盖equals的情景

equals方法是Object类自带的基本方法之一,也是一个非常常用的方法。如果满足了以下条件之一,那就直接继承而不要去重写它:

  1. 类的每个实例本质上唯一。
    比如Thread这种,所代表的不是值而是一个活动实体,应当使equals()方法保证能够区分它们。
  2. 无需为该类提供“逻辑相等”测试(简称“重写无意义”)。
    比如,虽然{@link java.util.regex.Pattern}可以重写equals来比较两个Patten匹配形式表达式是否相同,但感觉完全不会有什么地方会要求比对这个,所以这时候也完全无需重写这个方法。
  3. 父类已经重写过这个方法,而这对子类仍然适用。
    比如大多数Set继承了父类java.util.AbstractSet重写后的equals方法,Map , List同理。
  4. 类的访问权限是private或者缺省(包私有)的,而且确定它的equals不会被调用(简称“没用”)。当然如果还是不放心,也可以重写一下比如这样:public boolean equals(Object o) {throw new AssertionError();}

需要覆盖equals的情景

**该类(可以称为“值类”value class)有独特的逻辑相等判断方式(不同于对象等同),而父类的equals又没办法满足这一需求的时候。**这种情况通常发生在当类仅仅表示一个值的时候,比如Integer或者Date的比较,无需了解两者是否同一对象,而要知道其值是否等同。这时为满足需求,不仅需要覆盖equals()方法,还要保证该类的实例被用到map映射作“键”,或者在set集合中作为其中元素时符合逻辑,满足预期行为。

覆盖时需遵守的通用规约

当确认覆盖时,也必须遵守一定的通用规约,即在x,y不为null的前提下满足:

  1. 自反性:x.equals(x) == true
  2. 对称性:x.equals(y) == y.equals(x)
  3. 传递性:x.equals(y) && y.equals(z) => x.equals(z)
  4. 一致性:x.equals(y)在x与y的值没有改变的情况下返回结果不会变;
  5. 空值为false:x.quals(null) == false

如何写出高质量的equals方法(参照后文的例子进行理解)

  1. ==操作符检查“是否为这个对象的引用”。是则返回true.这只是一种性能优化,如果比较的代价很高可以考虑这么做。
  2. instanceof检查“参数类型是否正确”。正确指的是equals方法所在的那个类,或者该类所实现的某个接口。如果接口需要类实现经过改进的equals方法以允许跨类比较,那么就让接口作为第二个操作数。比如Set,Map,List这些,它们都实现了Collection接口,所以都应该这么干。
  3. 将参数强转为正确类型。由于之前的instanceof判断,所以这一步骤不会出现什么问题。
  4. 对该类中的每个“关键”域,检查它们是否相等(比如之前transitivity.Point中对坐标值的比较)。如果步骤2中的第二操作数是接口,就必须通过接口方法访问参数的字段;如果是类,就可以根据类中这些参数的访问权限,直接或间接地访问这些字段。
  • 对于float或者double类型,需要使用{@link Float#compare(float, float)}或者{@link Double#compare(double, double)}进行比较,否则因浮点型精度问题,可能会有“相等值比较返回fasle”这样的bug出现;而且对float和double的特殊值特殊处理是有必要的,因为存在NaN,0.0f这种常量。float与double之间的比较可以用{@link Float#equals(Object)}或者{@link Double#equals(Object)},但是由于涉及到自动装箱,效率会相对较低。对于数组来说,需要确认对应的每个元素都相同,这时如果能保证每个元素都是有效的,那么可以考虑使用{@link java.lang.reflect.Array#equals(Object)}
  • 有些情况下,null是合法的,为了防止抛出空指针异常,可以用{@link Object#equals(Object)}检测是否相同,有另一些情况下,对象之间的比较或许非常复杂繁琐耗时,那么可以考虑把一些“范式”置为常量存储,到时候直接跟这些范式比较,会省时很多。这对于一些不可变类非常管用。因为一旦类发生改变,范式也要做出相应变化。
  • equals方法性能受字段比较的顺序影响,应将最有可能不一样的、比较起来容易的字段,将其比较的顺序提前。并且,不应该比较不属于对象逻辑状态域之外的字段,比如用于同步操作的Lock域;也不要比较可以由关键字段计算得出的冗余数据,因为计算方式不变时,特定参数应该对应相同的结果,不过如果这种结果能体现该类的整体特征,换句话讲就是比较完这一个参数就能确定其他十几个参数是否相等时,当然可以先比较这个参数。比如对两个多边形类进行比较时,如果面积area不同,那么两个多边形必然不相同。
  1. 当编辑完equals方法之后,需要检查它是否满足自反、对称、一致三个特性(另外两个特性一般会自动满足),并且稍稍花些时间验证。
  2. 覆盖equals方法的同时也要覆盖hashCode()。

重写equals()

例子1:

//String.equals
public boolean equals(Object anObject) {[]
    if (this == anObject) {    //是否等于自身
        return true;
    }
    if (anObject instanceof String) {    //类型是否相等
        String anotherString = (String) anObject;    //转换类型
        int n = value.length;    
        if (n == anotherString.value.length) {    //先判断长度是否相等
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {    //一个一个字符判断值是否相等
                if (v1[i] != v2[i])
                        return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

例子2:

@Override
public boolean equals(Object obj) {
    if (!(obj instanceof BestPhoneNumber)) {
        return false;
    }
    BestPhoneNumber phoneNumber = (BestPhoneNumber) obj;

    return phoneNumber.number.equals(this.number) &&
            phoneNumber.sex == this.sex &&
            phoneNumber.intimacy == this.intimacy;
}

2. 重写equals()方法的同时也要重写hashCode()方法

必须在每个重写equals()的类中重写Object.hashCode(),否则将违反hashCode()的通用规约,然后在比如HashMap,HashSet这种类里面使用时,出现各种奇怪的异常。

hashCode通用规约

  1. 不改变在equals()中比较所涉及到的相关参数前提下,对某一对象多次执行hashCode, 它必须返回相同值,而该应用本次执行结束后,下一次执行跟本次执行时的返回值是否相同就没啥要求了。
  2. 如果x.equals(y) == true,那么x和y的hashCode()返回值也应相同。
  3. 如果x.equals(y) == false,x和y的hashCode()返回值不要求绝对不同。但程序员应清楚,存在不同对象有相同hashCode的情况。可以考虑改用更好的映射函数尽量减少这种情况,以提供更好性能。

案例

例子1:

@Override
public int hashCode(){
	int result = 31;
	result = 31 * result + number.hashCode();
	result = 31 * result + Float.hashCode(intimacy);
	result = 31 * result + Boolean.hashCode(sex);
	return result;
}

例子2:

@Override
public int hashCode()
{
      final int prime = 31;
      int result = 1;
      result = prime * result + age;
      result = prime * result + ((name == null) ? 0 :  name.hashCode());
      return result;
}

hashCode计算方法

  1. 随便搞个非零的常数,作为hashCode的乘数,这里用了23,放在hashCode()方法里面

  2. 对equals方法所设计需要比较的每一个参数f,按如下方式计算其局部哈希值c:
    对boolean, byte, char, short, int, long, float, double这些基本数据类型,使用Type.hashCode(f),其中Type是对应基本类型的装箱类型
    对引用类型并且其equals也使用递归比较字段的,那么也递归调用其hashCode()以计算c;如果要实现更复杂的比较,那么就为它设计一个“范式”,然后在范式中调用hashCode.另外,若该字段为null则c = 0(其实也可以是其他常数,但一般都用0)
    对于数组字段,将其中所有有效字段单独计算局部哈希值然后求和;如果没有有效字段,则使用常量,但这时最好就不要使用0了。另外,如果数组中全部元素都有效,考虑使用Array.hashCode()

  3. result = 31result + c,并将这个值返回
    (1). 选择31这个数字是因为它是奇素数,素数可以有效减少哈希值相同的情景,而奇数可以防止因位移而丢失信息(偶数为2的倍数,*2等价于向左移位)
    (2). result有一个非0的初始值,这样第二步中的0默认值就不会对它产生影响导致冲突增加
    (3). 有时需要基于参数顺序调整计算哈希值的参数,例如当计算String类型hashCode时,"add"与"dda"有不同返回值更有利于减少冲突。
    (4). 另外,数字31有个比较好的特性,就是可以通过位移和减法完成乘法操作(31i = i << 5 -i),这种优化很多虚拟机可以自动完成

3. Comparable & Generic

在这里插入图片描述

4. Clone

深拷贝和浅拷贝
如果是引用的深拷贝,要先拷贝引用,还要拷贝具体对象。

抽象类与接口的区别

参数抽象类接口
默认的方法实现可以有默认的方法实现接口完全是抽象的,不存在方法的实现
实现extends,如果子类不是抽象类,需要提供抽象类中所有声明的方法的实现implements,需要提供接口中所有声明的方法的实现
构造器可以有不可以
main方法可以有,并且可以运行没有main
多继承抽象方法可以继承一个类和实现多个接口只可以继承一个或多个其他接口
与正常java类的区别除了不能实例化抽象类之外,与普通java类没有区别接口是完全不同的类型

嵌套类和内部类

深入理解java嵌套类和内部类

1. 定义

嵌套类:在一个类的内部定义另一个类。
嵌套类包括静态嵌套类,非静态嵌套类(内部类)。内部类分为成员内部类、静态嵌套类、方法内部类、匿名内部类。
内部类的共性:

  • 内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的.class文件,但是前面冠以外部类的类命和$符号。
  • 内部类不能用普通的方式访问。内部类是外部类的一个成员,因此内部类可以自由地访问外部类的成员变量,无论是否是private的。

2. 静态嵌套类

在静态嵌套类内部,不能访问外部类的非静态成员,因为java中规定“静态方法不能直接访问非静态成员”。

public class StaticTest { 
	   private static String name = "javaJohn";         
    private String id = "X001";
    static class Person{
      private String address = "swjtu,chenDu,China";
      public String mail = "josserchai@yahoo.com";//内部类公有成员
      public void display(){
        //System.out.println(id);//不能直接访问外部类的非静态成员
        System.out.println(name);//只能直接访问外部类的静态成员
        System.out.println("Inner "+address);//访问本内部类成员。
      }
    }
  
    public void printInfo(){
      Person person = new Person();
      person.display();
      //System.out.println(mail);//不可访问
      //System.out.println(address);//不可访问
      System.out.println(person.address);//可以访问内部类的私有成员
      System.out.println(person.mail);//可以访问内部类的公有成员
    }
    public static void main(String[] args) {
    StaticTest staticTest = new StaticTest();
    staticTest.printInfo();
  }
}

3.

public class Outer { 
  int outer_x = 100; 
    class Inner{ 
      public int y = 10; 
      private int z = 9; 
      int m = 5; 
      public void display(){ 
        System.out.println("display outer_x:"+ outer_x); 
      } 
      private void display2(){ 
        System.out.println("display outer_x:"+ outer_x); 
      } 
    } 
    void test(){ 
      Inner inner = new Inner(); 
      inner.display(); 
      inner.display2(); 
      //System.out.println("Inner y:" + y);//不能访问内部内变量 
      System.out.println("Inner y:" + inner.y);//可以访问 
      System.out.println("Inner z:" + inner.z);//可以访问 
      System.out.println("Inner m:" + inner.m);//可以访问 
      InnerTwo innerTwo = new InnerTwo(); 
      innerTwo.show(); 
    } 
    class InnerTwo{ 
      Inner innerx = new Inner(); 
      public void show(){ 
        //System.out.println(y);//不可访问Innter的y成员 
        //System.out.println(Inner.y);//不可直接访问Inner的任何成员和方法 
        innerx.display();//可以访问 
        innerx.display2();//可以访问 
        System.out.println(innerx.y);//可以访问 
        System.out.println(innerx.z);//可以访问 
        System.out.println(innerx.m);//可以访问 
      } 
    } 
    public static void main(String args[]){ 
      Outer outer = new Outer(); 
      outer.test(); 
    } 
  } 

总结:
1、对于内部类,通常在定义类的class关键字前不加public 或 private等限制符,若加了没有任何影响。
2、内部类中可以直接访问外部类的数据成员和方法。
3、另外,就是要注意,内部类Inner及InnterTwo只在类Outer的作用域内是可知的,如果类Outer外的任何代码尝试初始化类Inner或使用它,编译就不会通过。同时,内部类的变量成员只在内部内内部可见,若外部类或同层次的内部类需要访问,需采用示例程序中的方法,不可直接访问内部类的变量。

4. 方法内部类

class Outer {
	public void doSomething() {
		class Inner {
			public void seeOuter() {}
		}
	}
}

A、方法内部类只能在定义该内部类的方法内实例化,不可以在此方法外对其实例化。
B、方法内部类对象不能使用该内部类所在方法的非final局部变量。

5. 匿名内部类

没有名字的内部类。表面上看起来似乎有名字,实际不是他的名字。

重写(Override)和重载(Overload)

重写(Override)和重载(Overload)详解

重写(Override)

重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为,也就是说子类能够根据需要实现父类的方法。
重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。
例子1:

class Animal {
	public void move() {
		System.out.println("Animal move.");
	}
}
class Dog extends Animal {
	public void move() {
		System.out.println("Dog move.");
	}
}
public class TestDog {
	public static void main(String args[]) {
		Animal a = new Animal();
		Animal b = new Dog();
		a.move();   // "Animal move."
		b.move();   // "Dog move."
	}
}

例子2:

class Animal {
	public void move() {
		System.out.println("Animal move.");
	}
}
class Dog extends Animal {
	public void move() {
		System.out.println("Dog move.");
	}
	public void bark() {
		System.out.println("Dog bark.");
	}
}
public class TestDog {
	public static void main(String args[]) {
		Animal a = new Animal();
		Animal b = new Dog();
		a.move();   // "Animal move."
		b.move();   // "Dog move."
		b.bark();   // Exception.
	}
}

改程序抛出编译错误,因为b的引用类型Animal没有bark方法。

方法的重写规则

  • 参数列表必须完全与被重写方法的相同;
  • 返回类型必须完全与被重写方法的返回类型相同;
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为final的方法不能被重写。
  • 声明为static的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个方法,则不能重写这个方法。

重载(Overload)

重载是在一个类里面,方法名字相同,而参数不同,返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用地方就是构造器的重载。
重载规则

  • 被重载的方法必须改变参数列表(参数个数或类型不一样)
  • 被重载的方法可以改变返回类型
  • 被重载的方法可以改变修饰符
  • 被重载的方法可以声明新的或更广的检查异常
  • 方法能够在同一类中或者在一个子类中被重载
  • 无法以返回值类型作为重载函数的区分标准
public class Overloading {
	public int test() {
		System.out.println("test 1");
		return 1;
	}
	public void test(int a) {
		System.out.println("test 2");
	}
	// 以下两个参数类型顺序不同
	public String test(int a, String s) {
		System.out.println("test 3");
		return "return test 3";
	}
	public String test(String s, int a) {
		System.out.println("test 4");
		return "return test 4";
	}
	public static void main(String[] args) {
		Overloading o = new Overloading();
		System.out.println(o.test());
		o.test(1);
		System.out.println(o.test(1, "test 3"));
		System.out.println(o.test("test 4"), 1);
	}
}

重写与重载之间的区别

区别点重载方法重写方法
参数列表必须修改一定不能修改
返回类型可以修改一定不能修改
异常可以修改可以减少或删除,一定不能抛出新的或者更广的异常
访问可以修改一定不能做更严格的限制(可以降低限制)

总结

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
(1) 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
(2) 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
(3) 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
在这里插入图片描述
在这里插入图片描述

装饰模式(Decorator Pattern)

1. 概念

动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

volatile和synchronized

volatile与synchronized的区别
1)volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
2)volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值