悲伤来袭

2010年3月26日 wtb 7 条评论

1)

这两天有点感冒,头脑不太好使。在从网上看到某学弟签名:

从前很羡慕昆明的天气,四季如春,来南京后才知道南京的天气更牛B,春如四季。

2)

第一次如此迫近的觉得学生生涯的倒计时。

是啊,就快这么结束了。

我开始有一丝丝后悔,课上的太少了,以至于都不太记得老师长什么样了。

他们管我叫二哥,对面宿舍有个一哥。

一哥从来不上课,我比他好点,我这学期去过一次。

唯一的区别是

一哥尚未落实单位

二哥即将奔赴上海,到某知名外企开始实习

3)

火箭同学是谷歌的忠实粉丝。

所以昨天他说只有某流氓政府才能将谷歌逼走。

虽然我比他小那么几个月,但是我决定立马摆出长辈的姿态来教育他。

谷歌要在中国做生意,起码得听党和政府的管束。

全中国上千万家企业做到了这点,凭什么他就不能做到。

中国多少家企业被美国设置贸易壁垒拒绝门外,也没见你为中国鸣冤

现在一家外企推出内地市场,你反倒叫嚣起来

他不赚国人的钱你不舒服么

谷歌无非也只是美国政府向中国施压的一枚棋子

有必要因为这个而诋毁自己的国家么

火箭至今依然愤愤不平

愤青何其多也

4)

老蔡QQ上给我看了张CY的照片,于是我就不可抑制的悲伤了

当年青春靓丽活泼可爱的美少女不见了

看着照片我尚能依稀记得当时她笑起来是多么甜

是真的很甜

楼B跟我说这个照片他早就看过了

可惜我却没有看过,如果今晚我没有跟老蔡聊天

伤心了,人总是在年龄的碾压下默默发生着变化~

没有什么比亲眼看着小萝莉一天天长大更让怪蜀黍感到难受

老蔡问我是不是还抱有幻想

我心里在嘀咕,他娘的什么叫幻想。。。

我说我们三年没见面了

是啊,这一晃,已经三年多了

太难过了,不写了

分类: 随笔 标签:

从hashCode开始漫谈

2010年3月23日 wtb 没有评论

今天翻“Effective Java”的时候看到了改写hashCode()方法的三条约定,突然想到了某些问题,故记录之。

1.在每个改写了equasl方法的类中,你也必须要改写hashCode方法。

2.如果equals相等,两个对象的hashCode必须相等。

3.不相等的对象倾向于产生不相等的散列码,但是不相等的对象可以有相等的散列码。

我在写代码的时候曾遇到过如下情况:

class Person{   
    String name;
    int age;   
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }   
    public boolean equals(Object o){       
        if(o instanceof Person){
            Person p=(Person)o;
            return name.equals(p.name) && age==p.age;
        }
        else return false;
    }
}

public class Test{   
    public static void main(String s){
        HashSet<Person> s=new HashSet<Person>(); 
        s.add(new Person("hello", 23));       
        System.out.println(

             s.contains(new Person("hello", 23)));                  
   }
}

这段代码很simple很naive,但是当看到结果的时候依然迷惑了~因为我所期望的结果是输出true,但很不幸的是这段代码的结果输出为false。更让我感到困惑的是,如果对这两个对象调用equals方法比较,他们必然是相等的。

于是我便去参阅jdk中HashSet类的源码,才知道原来HashSet其实就是一个HashMap。众所周知HashMap中存放的是键值对,因此所有的键都必须是唯一的,在HashMap中不可能同时存在两个相同的键。所以HashSet便是利用这个特性,将add进这个Set的对象都当做是Map的某个key。下面是HashSet实现类的代码片段:

public class HashSet<E>{
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();
    public HashSet() {
        map = new HashMap<E,Object>();
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    public boolean contains(Object o) {
        return map.containsKey(o);
    }
    ……

}

可以清楚的看出来,HashSet类中包含了一个map,而且是HashMap,同时还保存了一个空对象PRESENT。每当我们调用add方法的时候,实质上就是在hashMap中插入一对键值。此时充当键值的是我们想插入Set的元素E,而充当值的,正是PRESENT。其实这个结构挺囧的,因为HashMap中存放的所有值,都是这个PRESENT的引用。

参阅javaAPI手册,HashMap.put方法有一个返回值V。这个V是与key关联的旧值,如果key没有任何映射关系,则返回null。

public boolean add(E e) { 
       return map.put(e, PRESENT)==null;
}

// 首次调用add方法添加元素e的时候,map中会建立 e—>PRESENT 的键值对。

// 由于此前map中不存在e作为key,因此会返回一个null值,此时add方法调用成功,返回true。
// 首次调用add方法添加元素e的时候,map中已存在 e—>PRESENT ,因此会把这个PRESENT
// 当成是旧值返回,由于PRESENT!=null,故add方法返回false。

 

现在回归最开始的问题,这个问题可以归结如下。第一个输出true很好理解,第二个输出false有点费解。

public class Test{   
    public static void main(String s){
       
        Person p1=new Person("hello", 23);
        Person p2=new Person("hello", 23);
        System.out.println(p1.equals(p2));        //输出true
       
        HashSet<Person> s=new HashSet<Person>(); 
        s.add(p1);
        System.out.println(s.contains(p2));       //输出false   
   
    }
}

当执行完 s.add(p1) 之后,s中的map里已经包含了 P1 —>PRESENT 的键值对。然后对这个map进行查询,发现不含有p2键。这说明有两个equals相等的对象,正在一个hashMap中充当着不同的键,这很不合理!那么HashMap中的键究竟是根据什么来判断他们是否相同呢?不妨来研究一下HashMap类:

public class HashMap<K,V>{
    transient Entry[] table;    //这是一个Entry数组,注意Entry其实是一个链表

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value); //key是null的情况

 
        // 如果key不是null,首先调用hash方法进一步散列。然后通过indexFor函数将
        // 所得的hash码映射到 [0,table.length-1] 区间,得到的映射值为i。
        // 遍历table[i]这个链表,如果已经有某个Entry结点的key与所要插入的key相等,
        // 那么修改这个Entry结点,用value替换掉原来的旧值
        // 如果table[i]链表中尚不存在这样键为key的Entry,则生成一个这样的结点,
        // 插入到table[i]的首个结点之前       
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        } 
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    } 
 
    /**
     * 如果key是null,那么这个value一定是放在table[0]所在的链表里的
     * 如果table[0]这个链表中本来就存在Entry,那么会遍历这个链表,直到找到key是null的
     * 结点,用新的value替换掉旧的,返回旧值
     * 如果table[0]这个链表中为空,或者不存在key为null的Entry结点,那么会调用addEntry
     * 方法,该方法创建一个结点,插入到table[0]链表的首个结点之前
     */
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    } 
 
    //hash方法用于将对象的hashCode进一步散列,具体的算法看不懂
    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    static int indexFor(int h, int length) {
        return h & (length-1);
    }
}

阅读完HashMap的代码片段,至少能够确定一点,用对象A作为键去关联另一个对象B,可以说其实是建立了对象的A的散列码和对象B之间的映射。因此在上面一个set的例子中,两个Person对象P1与P2虽然用equals方法相等,但是由于没有覆写hashCode方法,所以P1和P2返回的散列码并不一样。故P1与P2可以在hashMap中充当不同的键。

回到刚开始的三点建议~当我们改写了equals方法的时候,还需要改写hashCode方法。并且需要保证如果equals方法相等,那么hashCode也一定要相等。这两条建议其实是强制保证我们代码的语义与真正执行结果之间的一致性。因为很多时候,equals方法并不简单比较引用是否相等,我们更需要的是一个能够进行对象内容比较的equals方法。所以如果我们认为两个对象相等了,正如P1和P2,那么我们要确保 s.add(p1)后s.contains(p2)一定能够返回true.

至于第三点建议,如果不相等对象产生了相同的散列码也没有问题。用不同散列码的好处就在于可以将键散列到Entry数组的不同位置,我们下次访问的时候,仅仅是访问数组的某一个单元,消耗的是单位时间。但是如果在hashMap中用这些相同散列码的对象作为键值,那么生成的键值对肯定在数组某个单元包含的同一串链表上,查找或者修改这样一个链表是很费时的。

分类: Java 标签:

处女成功面

2010年3月20日 wtb 9 条评论

注意,不是处女面成功。在我仅有的两回求职面试经历中,这是第一次成功的面试,故记录下来,以方便回忆。

Mr :先自我介绍一下

我 :英文的还中文的啊

Mr :中文的吧

我 :还是英文的吧,不然我白准备了

Mr :那开始吧

我 :……(此处省略3分钟)

Mr :how to play a good role in team development

我 :I‘ve never work on a large project with other people,so I don’t know

Mr :(指着我的简历)讲一讲你做过的项目吧

我 :所有的都要讲么

Mr :挑几个有代表性的讲讲

我 :……(此处省略3分钟)

Mr :JavaScript会不会

我 :会啊

Mr :那说说怎么用JS来判断浏览器的版本

我 :不知道

Mr :你不说会的么

我 :这个不会,我一般coding的时候会找些资料来参考

Mr :说说JS有哪些内置对象

我 :date,object,window,document,别的忘掉了

Mr :具体说说这些对象

我 :不知道

Mr :你对你自己的javascript水平打几分啊?如果满分是10分的话

我 :0分

Mr :据人推荐说你JAVA语言研究的很深,自己给自己打几分

我 :接近满分吧

Mr :具体几分

我 :8分吧

Mr :那好。谈一谈多线程。

我 :这个不知道。事实上,JAVA中除了多线程与IO,其余的概念我都很精通。

Mr :比如哪些概念?

我 :java中的一些高级概念,比如泛型,反射,collection,Swing,我都非常精通

Mr :那好。谈一谈反射。

我 :反射大体上来讲就是一种代码运行时能够动态分析类型的能力。java中的反射就是说在运行时可以解析java类,并且能够获取类中的所有信息,比如实例域,方法,甚至内部类,不管这些成员是公有的还是私有的。Java中所有的反射API都是在java.lang.reflect包下。但是谈起反射,首先要从Class类讲起。Class类是java.lang包中的,Class代表了java中定义的类型,所以严格上讲,Class类就是代表Type. 在java中,每个自定义的类,都可以被看成一种类型,因此都可以获得相对应的Class对象,此外,还有8种原类型,也有对应的Class对象,甚至Void类型,也有自己的Class对象。这些基本类型的Class对象都存活在jvm中,其他比如自定义类型的Class对象,都是jvm后来加载这些类型生成的。说白了,Class对象中,记录了某个类的所有信息,而反射就是通过这些Class对象来剖析类型的……

Mr :先停一停,不用具体讲了,举个例子,我怎么样用反射来获取类中定义的私有变量

我 :通过这个类的对象.getClass方法,可以获取这个类型对应的Class对象。然后通过调用getDeclaredField方法,来获取Field数组,Field就是java.lang.refelct包中的一个类,代表了一个类型中的所有实例域,包括私有的。然后用for循环来遍历这个Field数组。Field类有一个函数用来获取access modifier,具体的方法可能就叫getModifier,记不太清了,一查API就知道了。通过判断这个函数的返回值,可以知道是不是private的,如果是的话,单独保存这个Field就行了

Mr :好的,对虚拟机了解多少

我 :蛮多的

Mr :谈一谈classloader

我 :不太清楚。大体上我只知道有三种classloader。但是具体的还没研究过。不过我可以讲一讲一个类型是如何被加载和初始化的。

Mr :说说

我 :类型被加载分成三大步骤。第一步是装在,然后是连接,最后是初始化。第一步就是jvm将编译完的class文件读取进来。第二步又分成三小步,1 验证 2 准备 3 解析。验证就是说判断读进来的2进制字符流是否合法,准备阶段提取出所有的实例域,然后为他们赋初始值,例如整型赋0,引用类型赋null,这是java比C++改进的一点。解析阶段是可选的。在最后的初始化阶段,虚拟机会提取类型中的所有静态实例域,以及所有的静态初始化块,并且根据他们在代码中的上下顺序,这很重要,来依次执行

Mr :这是看的哪的

我 :深入java虚拟机

Mr :好,谈一谈垃圾回收

我 :垃圾回收就是为了清除不用的对象,释放出更多的内存空间给程序运行使用。在垃圾回收机制中,最简单的实现方法就是利用引用计数,我印象中,Python中就是利用这种机制实施的。具体说来就是分别为内存中的每一个都记录下一个数字,这个数字表示当前有多少引用指向这个对象,如果说改数字为0,那么这个对象肯定不是活的,应该被列入垃圾回收的名单。这个机制有一个缺点,当两个或者多个对象之中出现了循环引用,那么这个机制就无法很好的工作,没有办法将这两个对象都回收掉。高级一点的做法就是,因为程序运行在栈区中。比如当前栈区有一个引用指向某个对象,然后该对象用包含指向其他对象的引用。也就是说,如果一个堆中的对象,我们通过一层一层的连接,能够找到它当前在栈区的一个引用,那么这个对象肯定是有用的,也就是不能被回收的。如果我们找不到这样一条连接,那么这个对象就不是活的,可以被回收掉。sun的jvm实现了这个功能。除此之外,还有一种被称为复制—拷贝的做法。将内存中的堆,分成几个大区。由于对象都分配在堆中,当某个堆区的内存不够用时候,将这个堆区对象复制到另外的堆区,在复制的时候将这些对象重新紧密排列。这样可以有效的回收内存中的空间碎片,这样腾空的内存区域又可以用来存放新的对象。总体上来说,一般如果内存很大,足够用的时候,不会考虑到垃圾回收的,因为这么做肯定会导致当前运行的程序停下来,转由JVM去执行回收。sun的虚拟机也实现了这个策略。

Mr :好。下面谈一谈别的。如果出现了争执,比如你和别的人,怎么协调

我 :我一般不跟人吵架

Mr :技术上出现分歧时很正常的,如果有了技术上的分歧怎么办

我 :这好办,谁对听谁的

Mr :不知道谁对的

我 :小牛听大牛

Mr :说一说你认为谁比较强

我 :(指了指面试的名单)这个上面的人都不错

Mr :你有什么要问我的么

我 :结果什么时候可以出来

Mr :一周以内吧

我 :我这个简历要不要重写。

Mr :太简单了点,写的稍微具体点,改完了发一份到我邮箱。

我 :好的

分类: 随笔 标签:

4.1带边界的通配符

2010年3月19日 wtb 没有评论

来考虑一个简单的画图程序。这个程序可以用来绘制矩形或者圆形。你可以用下面的代码来定义这些图形的层次:

1
2
3
4
5
6
7
8
9
10
11
public abstract class Shape {
    public abstract void draw(Canvas c);
}
public class Circle extends Shape {
    private int x,y,radius;
    public void draw(Canvas c) { ... }
}
public class Rectangle extends Shape {
    private int x,y,width,height;
    public void draw(Canvas c) { ... }
}

所有的图形都可以被绘制在一块画布上:

1
2
3
4
5
public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
    }
}

任何一次绘制都需要用到一个Shape对象。假设我们将这些对象都存放在一个List中,这样可以用一个方法很方便的绘制出List中的所有的Shape:

1
2
3
4
5
public void drawAll(List< Shape> shapes) {
    for (Shape s:shapes) {
        s.draw(this);
    }
}

现在,drawAll()方法仅仅能被Shape类型的List所调用。注意,List必须是确切的Shape类型,所以,这个方法并不能被诸如List<Circle>所调用。这是很不幸的事情,既然该方法所作的所有事情就是从列表中读取Shape,那么它理当能够被List<Circle>调用。我们真正需要的其实是一个能够接受所有Shape类型列表的方法:

1
public void drawAll(List< ? extends Shape> shapes) { ... }

这儿有一个很小,但是很重要的区别:我们用List<? extends Shape>替换了原来的List<Shape>。这样drawAll()方法就能够接受所有Shape的子类型。因此,现在我们能够用List<Circle>去调用该方法了。

List<? extends Shape>是一个带边界通配符的示例。其中的’?’代表了一种不知道的类型,就像我们之前看到的通配符一样。但是在这里,我们知道这个未知的类型一定是Shape的一种子类型。我们说Shape是这儿通配符的上界。

在我们获得使用带边界通配符所带来的弹性时,通常需要付出一些代价。这里的代价就是,我们再也不能向Shapes中写入元素。举例来说:

1
2
3
public void addRectangle(List< ? extends Shape> shapes) {
    shapes.add(0, new Rectangle()); //compile-time error!
}

这样写是错误的。shapes.add()方法的第二个参数类型是:? extends Shape 。这是一种Shape类型的未知子类型。既然我们不知道这个子类型是什么,我们当然也不知道这个子类型究竟是不是Rectangle的父类型。因此向add方法传递一个Rectangle类型的参数当然是不安全的。

带边界的通配符正是我们在车管所例子中所需要的。例子中假设,数据是map结构进行展示的,键值对是names(String类型)和people(Person类型或者其子类型,如Driver)。

1
2
3
4
5
6
public class Census {
    public static void addRegistry(Map< String,? extends Person> registry)
    { ...}
} ...
Map< String,Driver> allDrivers=...;
Census.addRegistry(allDrivers);

4.通配符

2010年3月17日 wtb 没有评论

来考虑一个问题,假设我们要写一个程序,这个程序用来打印一个集合中的所有元素。如果用Java旧版本,你也许会像下面这样写:

1
2
3
4
5
6
void printCollection(Collection c) {
    Iterator i=c.iterator();
    for (k=0;k< c.size();k++) { 
        System.out.println(i.next()); 
    }
}

这里还有一份利用泛型(以及新的for循环)重写的简单版本:

1
2
3
4
5
void printCollection(Collection< Object> c) {
    for (Objecte:c) {
        System.out.println(e);
    }
}

问题是这份新版本的代码不如旧版本的代码有用。因为我们可以用任何种类的集合去调用旧版本的代码,而新版本的代码只能用Collection<Object> 去调用。Collection<Object>已经被我们刚才证实过了,它并不是所有集合的超类型。

那么所有集合的超类型是什么呢?是Collection<?>,这是一种类型参数可以匹配任何元素的集合, 因此被称为通配符。我们可以写成:

1
2
3
4
5
void printCollection(Collection< ?> c) {
    for (Objecte:c) {
       System.out.println(e);
    }
}

现在,我们可以用任何类型的collection进行调用。注意在这个函数内部:我们依然从c中读取集合中元素,并且将其赋予Object类型。这样总是安全的,不管集合中存放的元素实际类型是什么。然而向一个Collection<?>中添加object并不是安全的:

1
2
Collection< ?> c=newArrayList();
c.add(new Object()); //compile time error

因为我们并不知道c 中元素的类型,所以并不能向其中添加一个object。注意,collection接口的add()方法的参数是类型E,E代表集合元素的类型。而这里集合元素的类型被声明为‘?’,‘?’表示什么类型呢?它代表未知的类型。我们传向add方法的参数必须是这个未知类型的一种子类型。既然我们不知道这是一种什么样的类型,我们当然不能传递给它任何东西,除非null。

另外一方面,对于给定的List<?>,我们可以调用get()并且使用返回结果。虽然返回的结果类型是未知的,但我们总归知道,它一定是个object。因此,将get()返回的结果赋给Object类型的引用,或者当做参数传递给需要Object的地方,这么做一定是安全的。