除了官方的hadoop版本外,cloudera.com也发布自己的hadoop版本。据说yahoo 负责hadoop开发的某个哥们跳槽到cloudera.com(hugo告诉我的)。为此,我把hadoop从官方的hadoop-0.20.1换成cloudera.com的hadoop-0.20.1+152.tar.gz,再加上hive-0.4.0+14.
配置方法跟官方的版本一致,具体请参考前边的文章或者网上的文档。可以当我配置好以后,运行start-all.sh时,确出现如下的报错:
======================================================================+
| Error: JAVA_HOME is not set and Java could not be found |
+———————————————————————-+
| Please download the latest Sun JDK from the Sun Java web site |
| > http://java.sun.com/javase/downloads/ < |
| |
| Hadoop requires Java 1.6 or later. |
| NOTE: This script will find Sun Java whether you install using the |
| binary or the RPM based installer. |
+======================================================================+
明明在hadoop-env.sh设置好了,为了保险,又在/etc/profile设置了一遍。以hadoop用户登陆系统,在任意路径执行echo $JAVA_HOME ,显示"/usr/local/jdk",是我服务器jdk正确的路径啊!再查一下文档,云:需要jdk1.6以上的版本,我使用的是jdk1.6.0_16,应该符合要求的。
那问题又会在哪里呢?grep一把,看那些文件包含字符串JAVA_HOME.先搜hadoop/conf,只有hadoop-env.sh包含这个字串。再换一个目录hadoop/bin,查看一下,其输出如下:
[root@hadoops2 hadoop]# grep JAVA_HOME bin/*bin/hadoop:export JAVA_HOME=/usr/local/jdkbin/hadoop:# JAVA_HOME The java implementation to use. Overrides JAVA_HOME.
bin/hadoop:if [ "$JAVA_HOME" != "" ]; then
bin/hadoop: #echo "run java in $JAVA_HOME"
bin/hadoop: JAVA_HOME=$JAVA_HOME
bin/hadoop:if [ "$JAVA_HOME" = "" ]; then
bin/hadoop: echo "Error: JAVA_HOME is not set."
bin/hadoop:JAVA=$JAVA_HOME/bin/java
bin/hadoop:CLASSPATH=${CLASSPATH}:$JAVA_HOME/lib/tools.jar
bin/hadoop-config.sh:if [ -z "$JAVA_HOME" ]; then
bin/hadoop-config.sh: export JAVA_HOME=$candidate
bin/hadoop-config.sh: if [ -z "$JAVA_HOME" ]; then
bin/hadoop-config.sh:| Error: JAVA_HOME is not set and Java could not be found |
bin/rcc:# JAVA_HOME The java implementation to use. Overrides JAVA_HOME.
bin/rcc:if [ "$JAVA_HOME" != "" ]; then
bin/rcc: #echo "run java in $JAVA_HOME"
bin/rcc: JAVA_HOME=$JAVA_HOME
bin/rcc:if [ "$JAVA_HOME" = "" ]; then
bin/rcc: echo "Error: JAVA_HOME is not set."
bin/rcc:JAVA=$JAVA_HOME/bin/java
bin/rcc:CLASSPATH=${CLASSPATH}:$JAVA_HOME/lib/tools.jar
嘿!好几个文件都与JAVA_HOME相关呢。接着挨个查看文件,感觉hadoop-config.sh这个文件嫌疑最大,其内容刚好有一段与运行hadoop报错内容相一致,我把程序片段贴在下面:
# attempt to find java
if [ -z "$JAVA_HOME" ]; then
for candidate in \
/usr/lib/jvm/java-6-sun \
/usr/lib/j2sdk1.6-sun \
/usr/local/jdk \
/usr/java/jdk1.6* \
/usr/java/jre1.6* \
/Library/Java/Home ; do
if [ -e $candidate/bin/java ]; then
export JAVA_HOME=$candidate
break
fi
done
# if we didn't set it
if [ -z "$JAVA_HOME" ]; then
cat 1>&2 <
| Error: JAVA_HOME is not set and Java could not be found |
+----------------------------------------------------------------------+
| Please download the latest Sun JDK from the Sun Java web site |
| > http://java.sun.com/javase/downloads/ < |
| |
| Hadoop requires Java 1.6 or later. |
| NOTE: This script will find Sun Java whether you install using the |
| binary or the RPM based installer. |
+======================================================================+
EOF
exit 1
fi
fi
我在这段中间加如行 ” /usr/local/jdk \”,然后在运行,一切就正常了!
作者:田逸(sery@163.com) from http://blog.formyz.org/?p=140
昨晚大家在说到spring配置文件中bean的id和name是纷纷表示出了迷惑。作为程序员有惑就得解惑,为此俺baidu了一把,google了一下找到相关同仁的文章,加上实践了一把,特在此涂鸦一把。
1、name与id之间的关系,name中文意思是名字,在这里就是一个bean定义的别名,默认等于id值。
2、id命名必须满足XML的命名规范,因为id其实是XML中就做了限定的。总结起来就相当于一个Java变量的命名:不能以数字,符号打头,不能有空格,如123,?ad,”ab “等都是不规范的,Spring在初始化时就会报错,诸如:
org.xml.sax.SAXParseException: Attribute value ”?ab” of type ID must be a name.
3、name属性则没有这些限定,你可以使用几乎任何的名称,如?ab,123等,但不能带空格,如”a b”,” abc”,,这时,虽然初始化时不会报错,但在getBean()则会报出诸如以下的错误:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named ’a b’ is defined
4、在spring同一个上下文中id,name只能有一个,但是同一个id可以用多个别名,比如<bean id=”a” name=”a1,a2,a3″ class=”com.shaxiaozi.A”/>,a1,a2,a3指向的是同一个对象[前提是单例的情况下]。如果不是单例则创建了和name数量相等的bean实体。
两个id相同的<bean>,否则在初始化时即会报错,如:
org.xml.sax.SAXParseException: Attribute value ”aa” of type ID must be unique within the document.
5、如果id和name都没有指定,则用类全名作为name,如<bean>,则你可以通过 getBean(“com.shaxiaozi.A”)返回该实例。
6、如果存在多个id和name都没有指定,且实例类都一样的<bean>,如:
<bean class=”com.stamen.BeanLifeCycleImpl”/>
<bean class=”com.stamen.BeanLifeCycleImpl”/>
<bean class=”com.stamen.BeanLifeCycleImpl”/>
调用方式依次:
getBean(“com.stamen.BeanLifeCycleImpl”)
getBean(“com.stamen.BeanLifeCycleImpl#1″)
getBean(“com.stamen.BeanLifeCycleImpl#2″)
总结:奇技淫巧,只为增广见识,id已经可以解决80%的问题了,name几乎没有出场的机会。
在xml配置文件中,autowire有5种类型,可以在<bean/>元素中使用autowire属性指定:
[xml]
[/xml]
相比bean而言beans少了最后一种装配方式default
可以设置bean使自动装配失效:
采用xml格式配置bean时,将<bean/>元素的autowire-candidate属性设置为false,这样容器在查找自动装配对象时,将不考虑该bean,即它不会被考虑作为其它bean自动装配的候选者,但是该bean本身还是可以用来注入到其它bean的。
现在做java web开发的大家用的说的最多的还是ssh,我在面试人的时候问的最多的也是ssh。如果有人问我spring管理的bean是单例的吗?我会毫不犹豫的回答Yes。那么能让它不是单例的吗?继续Yes,不过有点心虚了,毕竟没有专门去深入了解过,spring只是工作中的工具,以上答案也纯粹是经验之谈。
毕竟实践是检验真理的唯一标准,为此我就实践一把 :“spring单例yes or no”?。
<!–shaxiaozi.applicationContext.xml–>
<?xml version=”1.0″ encoding=”UTF-8″?>
<!DOCTYPE beans PUBLIC “-//SPRING//DTD BEAN//EN” “http://www.springframework.org/dtd/spring-beans.dtd”>
<beans>
<bean id=”sTest” class=”com.shaxiaozi.spring.Test”>
<property name=”initFunction” value=”singleton”/>
</bean>
<bean id=”nTest” class=”com.shaxiaozi.spring.Test” singleton=”false”>
<property name=”initFunction” value=”no_singleton”/>
</bean>
</beans>
[java]
package com.shaxiaozi.spring;
/**
* 待初始化类 根据aTest与bTest的值来判断spring的bean管理方式
* @author shaxiaozi
* @version 1.0
**/
public class Test {
private static int aTest;//检测非单例初始化两次
private int bTest;
private String initFunction = “new”;
public STest(){
aTest++;
bTest++;
}
public void printTest(){
bTest++;
System.out.println(“initFunction:”+initFunction);
System.out.println(“static aTest:”+aTest);
System.out.println(“bTest:”+bTest);
}
public void setInitFunction(String initFunction){
this.initFunction = initFunction;
}
}
[/java]
[java]
package com.shaxiaozi.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
*
* 用new来初始化测试执行类
* @author shaxiaozi
* @version 1.0
**/
public class SingletonTest {
public static void main(String[] args) {
//传统方式初始化实体
Test test1 = new Test();
Test test2 = new Test();
System.out.println(“test1==test2):”+(test1==test2));
test1.printTest();
test2.printTest();
}
}
[/java]
执行结果:
initFunction:new
static aTest:1
bTest:2
initFunction:new
static aTest:2
bTest:2
test1==test2:false
结论:bTest被初始化两次,彼此自增一次,aTest也被初始化两次,且test1!=test2,所以new出来的对象是两个不同的实体,引用的不是同一块内存地址。
[java]
package com.shaxiaozi.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
*
* spring默认配置初始化对象测试执行类
* @author shaxiaozi
* @version 1.0
**/
public class SingletonTest {
private static final ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
“shaxiaozi.applicationContext.xml”);
public static void main(String[] args) {
//spring默认配置初始化实体
Test test1 = (Test)applicationContext.getBean(“sTest”);
Test test2 = (Test)applicationContext.getBean(“sTest”);
System.out.println(“test1==test2):”+(test1==test2));
test1.printTest();
test2.printTest();
}
}
[/java]
执行结果:
initFunction:singleton
static aTest:1
bTest:2
initFunction:singleton
static aTest:1
bTest:3
test1==test2:true
结论:bTest被初始化一次,自增两次,aTest也被初始化一次,且test1==test2,所以通过spring bean默认配置获得是单例对象,只在加载application文件的时候被初始化,引用的是同一块内存地址。
[java]
package com.shaxiaozi.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
*
* spring 配置singleton=”false”初始化对象测试执行类
* @author shaxiaozi
* @version 1.0
**/
public class SingletonTest {
private static final ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
“shaxiaozi.applicationContext.xml”);
public static void main(String[] args) {
//spring配置singleton=”false”初始化实体
Test test1 = (Test)applicationContext.getBean(“nTest”);
Test test2 = (Test)applicationContext.getBean(“nTest”);
System.out.println(“test1==test2):”+(test1==test2));
test1.printTest();
test2.printTest();
}
}
[/java]
执行结果:
initFunction:no_singleton
static aTest:2
bTest:2
initFunction:no_singleton
static aTest:3
bTest:2
test1==test2:false
结论:bTest被初始化三次,自增三次,aTest也被初始化三次,且test1!=test2,所以通过spring bean配置singleton=”false”获得总是新的对象引用。
spring单例yes or no?没有答案,场景不同,答案不同,spring默认是单例的,也可以设置成非单例,实际应用场景一般是默认单例。个人浅见:如果不是单例的最好不要用spring去管理,通过第三段代码的执行结果可以看到,在spring加载的时候被初始化的对象,并没有被引用到,导致了无谓的开销。
在Eclipse里 export 选 JavaDoc,在向导的最后一页的Extra JavaDoc Options 里填上参数即可
比如项目采用的是UTF-8的编码就填:-encoding UTF-8 -charset UTF-8
@Override是什么?jdk1.5及以上版本用过eclipse自动生成接口实现类的程序员,应该会觉得眼熟的,它是Annotation,中文意思是注解。
@Override是干什么的?顾名思义,覆盖,就是覆盖父类的实现。
为什么问它是否是鸡肋?我上面说过,在jdk1.5及以上版本用eclipse生成接口实现类,其中的方法会给你自动标注上@Override,我们一般认为那是实现而不是覆盖,因为接口本身是没有实现方法的,但是却被标注成@Override,在中文开发者(主要是指傻小子本人)而言会觉得那是鸡肋,看到就删除,删除还删除。
某天,傻小子再重构代码,改了下接口方法的名字,发现实现类报错了。究其原因,接口方法名改了,相当于新增了一个方法,那么就需要我们实现了就应该好。理想是美好的,现实是残酷的。编译还是通不过,发现了罪魁祸首@Override。删除掉它,就好了,编译通过了。真的好了吗,明显是自欺欺人。代码是编译通过了,但是这不是多出一个不会被调用的无主孤魂了吗?没有在接口被声明,不会被外部调用的方法,这不是浪费吗?
经此往后,傻小子不得不感慨,存在即合理的。
@Override既然存在了,那么一定是有它的用处的,这里就告诉我们了,它能够自我检测被标注的实现是否存在于接口中。
名字解析:
@Override注释能实现编译时检查,你可以为你的方法添加该注释,以声明该方法是用于覆盖父类中的方法。如果该方法不是覆盖父类的方法,将会在编译时报 错。
Ctrl+1 快速修复(最经典的快捷键,就不用多说了)
Ctrl+D: 删除当前行
Ctrl+Alt+↓ 复 制当前行到下一行(复制增加)
Ctrl+Alt+↑ 复制当前行到上一行(复制增加)
Alt+↓ 当前行和下面一行交互位置(特别实用, 可以省去先剪切,再粘贴了)
Alt+↑ 当前行和上面一行交互位置(同上)
Alt+← 前一个编辑的页面
Alt+→ 下一个编辑 的页面(当然是针对上面那条来说了)
Alt+Enter 显示当前选择资源(工程,or 文件 or文件)的属性
Shift+Enter 在 当前行的下一行插入空行(这时鼠标可以在当前行的任一位置,不一定是最后)
Shift+Ctrl+Enter 在当前行插入空行(原理同上条)
Ctrl+Q 定 位到最后编辑的地方
Ctrl+L 定位在某行 (对于程序超过100的人就有福音了)
Ctrl+M 最大化当前的Edit或 View (再按则反之)
Ctrl+/ 注释当前行,再按则取消注释
Ctrl+O 快速显示 OutLine
Ctrl+T 快速 显示当前类的继承结构
Ctrl+W 关闭当前Editer
Ctrl+K 参照选中的Word快速定位到下一个
Ctrl+E 快速 显示当前Editer的下拉列表(如果当前页面没有显示的用黑体表示)
Ctrl+/(小键盘) 折叠当前类中的所有代码
Ctrl+×(小 键盘) 展开当前类中的所有代码
Ctrl+Space 代码助手完成一些代码的插入(但一般和输入法有冲突,可以修改输入法的热键,也可以暂用 Alt+/来代替)
Ctrl+Shift+E 显示管理当前打开的所有的View的管理器(可以选择关闭,激活等操作)
Ctrl+J 正 向增量查找(按下Ctrl+J后,你所输入的每个字母编辑器都提供快速匹配定位到某个单词,如果没有,则在stutes line中显示没有找到了,查一 个单词时,特别实用,这个功能Idea两年前就有了)
Ctrl+Shift+J 反向增量查找(和上条相同,只不过是从后往前查)
Ctrl+Shift+F4 关 闭所有打开的Editer
Ctrl+Shift+X 把当前选中的文本全部变味小写
Ctrl+Shift+Y 把当前选中的文本全部变为 小写
Ctrl+Shift+F 格式化当前代码
Ctrl+Shift+P 定位到对于的匹配符(譬如{}) (从前面定位后面时,光标要 在匹配符里面,后面到前面,则反之)
下面的快捷键是重构里面常用的,本人就自己喜欢且常用的整理一下(注:一般重构的快捷键都是 Alt+Shift开头的了)
Alt+Shift+R 重命名 (是我自己最爱用的一个了,尤其是变量和类的Rename,比手工方法能节省很多 劳动力)
Alt+Shift+M 抽取方法 (这是重构里面最常用的方法之一了,尤其是对一大堆泥团代码有用)
Alt+Shift+C 修 改函数结构(比较实用,有N个函数调用了这个方法,修改一次搞定)
Alt+Shift+L 抽取本地变量( 可以直接把一些魔法数字和字符串抽取 成一个变量,尤其是多处调用的时候)
Alt+Shift+F 把Class中的local变量变为field变量 (比较实用的功能)
Alt+Shift+I 合 并变量(可能这样说有点不妥Inline)
Alt+Shift+V 移动函数和变量(不怎么常用)
Alt+Shift+Z 重构的后悔药 (Undo)
编辑
作用域 功能 快捷键
全局 查找并替换 Ctrl+F
文本编辑器 查找上一 个 Ctrl+Shift+K
文本编辑器 查找下一个 Ctrl+K
全局 撤销 Ctrl+Z
全局 复制 Ctrl+C
全 局 恢复上一个选择 Alt+Shift+↓
全局 剪切 Ctrl+X
全局 快速修正 Ctrl1+1
全局 内容辅 助 Alt+/
全局 全部选中 Ctrl+A
全局 删除 Delete
全局 上下文信息 Alt+?
Alt+Shift+?
Ctrl+Shift+Space
Java 编辑器 显示工具提示描述 F2
Java编辑器 选择封装元素 Alt+Shift+↑
Java编辑器 选择上一个元 素 Alt+Shift+←
Java编辑器 选择下一个元素 Alt+Shift+→
文本编辑器 增量查找 Ctrl+J
文 本编辑器 增量逆向查找 Ctrl+Shift+J
全局 粘贴 Ctrl+V
全局 重做 Ctrl+Y
查 看
作用域 功能 快捷键
全局 放大 Ctrl+=
全局 缩小 Ctrl+-
窗口
作用 域 功能 快捷键
全局 激活编辑器 F12
全局 切换编辑器 Ctrl+Shift+W
全局 上一个编辑 器 Ctrl+Shift+F6
全局 上一个视图 Ctrl+Shift+F7
全局 上一个透视图 Ctrl+Shift+F8
全 局 下一个编辑器 Ctrl+F6
全局 下一个视图 Ctrl+F7
全局 下一个透视图 Ctrl+F8
文本编辑器 显示 标尺上下文菜单 Ctrl+W
全局 显示视图菜单 Ctrl+F10
全局 显示系统菜单 Alt+-
导 航
作用域 功能 快捷键
Java编辑器 打开结构 Ctrl+F3
全局 打开类型 Ctrl+Shift+T
全 局 打开类型层次结构 F4
全局 打开声明 F3
全局 打开外部javadoc Shift+F2
全局 打开资 源 Ctrl+Shift+R
全局 后退历史记录 Alt+←
全局 前进历史记录 Alt+→
全局 上一 个 Ctrl+,
全局 下一个 Ctrl+.
Java编辑器 显示大纲 Ctrl+O
全局 在层次结构中打开类 型 Ctrl+Shift+H
全局 转至匹配的括号 Ctrl+Shift+P
全局 转至上一个编辑位置 Ctrl+Q
Java 编辑器 转至上一个成员 Ctrl+Shift+↑
Java编辑器 转至下一个成员 Ctrl+Shift+↓
文本编辑器 转至 行 Ctrl+L
搜索
作用域 功能 快捷键
全局 出现在文件中 Ctrl+Shift+U
全 局 打开搜索对话框 Ctrl+H
全局 工作区中的声明 Ctrl+G
全局 工作区中的引用 Ctrl+Shift+G
文 本编辑
作用域 功能 快捷键
文本编辑器 改写切换 Insert
文本编辑器 上滚行 Ctrl+↑
文本编辑器 下 滚行 Ctrl+↓
文件
作用域 功能 快捷键
全局 保存 Ctrl+X
Ctrl+S
全 局 打印 Ctrl+P
全局 关闭 Ctrl+F4
全局 全部保存 Ctrl+Shift+S
全局 全部关 闭 Ctrl+Shift+F4
全局 属性 Alt+Enter
全局 新建 Ctrl+N
项目
作 用域 功能 快捷键
全局 全部构建 Ctrl+B
源代码
作用域 功能 快捷键
Java编辑 器 格式化 Ctrl+Shift+F
Java编辑器 取消注释 Ctrl+\
Java编辑器 注释 Ctrl+/
Java 编辑器 添加导入 Ctrl+Shift+M
Java编辑器 组织导入 Ctrl+Shift+O
Java编辑器 使用 try/catch块来包围 未设置,太常用了,所以在这里列出,建议自己设置。
也可以使用Ctrl+1自动修正。
运 行
作用域 功能 快捷键
全局 单步返回 F7
全局 单步跳过 F6
全局 单步跳入 F5
全局 单步跳入 选择 Ctrl+F5
全局 调试上次启动 F11
全局 继续 F8
全局 使用过滤器单步执行 Shift+F5
全 局 添加/去除断点 Ctrl+Shift+B
全局 显示 Ctrl+D
全局 运行上次启动 Ctrl+F11
全局 运行 至行 Ctrl+R
全局 执行 Ctrl+U
重构
作用域 功能 快捷键
全局 撤销重 构 Alt+Shift+Z
全局 抽取方法 Alt+Shift+M
全局 抽取局部变量 Alt+Shift+L
全局 内 联 Alt+Shift+I
全局 移动 Alt+Shift+V
全局 重命名 Alt+Shift+R
全局 重 做 Alt+Shift+Y
1、mvn archetype:create -DgroupId=org.david.app -DartifactId=mywebapp -DarchetypeArtifactId=maven-archetype-webapp
2、cd mywebapp
mvn eclipse:eclipse
导入eclipse工程
(或者直接从eclipse中导入maven工程)
3、添加servlet依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
4、添加源代码目录src/main/java
将源代码放在该目录下。
5、添加jetty插件
<build>
<finalName>mywebapp</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
</plugin>
</plugins>
</build>
6、用jetty调试
命令行:mvn jetty:run
译者注 :你可能会觉得Java很简单,Object的equals实现也会非常简单,但是事实并不是你想象的这样,耐心的读完本文,你会发现你对Java了解的是如此的少。如果这篇文章是一份Java程序员的入职笔试,那么不知道有多少人会掉落到这样的陷阱中。原文转自http://www.artima.com/lejava/articles/equality.html 三位作者都是不同领域的大拿,有兴趣的读者可以从上面这个连接直接去阅读原文。
摘要
本文描述重载equals方法的技术,这种技术即使是具现类的子类增加了字段也能保证equal语义的正确性。
在《Effective Java》的第8项中,Josh Bloch描述了当继承类作为面向对象语言中的等价关系的基础问题,要保证派生类的equal正确性语义所会面对的困难。Bloch这样写到:
除非你忘记了面向对象抽象的好处,否则在当你继承一个新类或在类中增加了一个值组件时你无法同时保证equal的语义依然正确
在《Programming in Scala》中的第28章演示了一种方法,这种方法允许即使继承了新类,增加了新的值组件,equal的语义仍然能得到保证。虽然在这本书中这项技术是在使用Scala类环境中,但是这项技术同样可以应用于Java定义的类中。在本文中的描述来自于Programming in Scala中的文字描述,但是代码被我从scala翻译成了Java
常见的等价方法陷阱
java.lang.Object 类定义了equals这个方法,它的子类可以通过重载来覆盖它。不幸的是,在面向对象中写出正确的equals方法是非常困难的。事实上,在研究了大量的Java代码后,2007 paper的作者得出了如下的一个结论:
几乎所有的equals方法的实现都是错误的!
这个问题是因为等价是和很多其他的事物相关联。例如其中之一,一个的类型C的错误等价方法可能意味着你无法将这个类型C的对象可信赖的放入到容器中。比如说,你有两个元素elem1和elem2他们都是类型C的对象,并且他们是相等,即elem1.equals(elm2)返回ture。但是,只要这个equals方法是错误的实现,那么你就有可能会看见如下的一些行为:
Set hashSet<C> = new java.util.HashSet<C>();
hashSet.add(elem1);
hashSet.contains(elem2); // returns false!
当equals重载时,这里有4个会引发equals行为不一致的常见陷阱:
1. 定义了错误的equals方法签名(signature) Defining equals with the wrong signature.
2. 重载了equals的但没有同时重载hashCode的方法。 Changing equals without also changing hashCode.
3. 建立在会变化字域上的equals定义。 Defining equals in terms of mutable fields.
4. 不满足等价关系的equals错误定义 Failing to define equals as an equivalence relation.
在剩下的章节中我们将依次讨论这4中陷阱。
陷阱1:定义错误equals方法签名(signature)
考虑为下面这个简单类Point增加一个等价性方法:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
// …
}
看上去非常明显,但是按照这种方式来定义equals就是错误的。
// An utterly wrong definition of equals
public boolean equals(Point other) {
return (this.getX() == other.getX() && this.getY() == other.getY());
}
这个方法有什么问题呢?初看起来,它工作的非常完美:
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point q = new Point(2, 3);
System.out.println(p1.equals(p2)); // prints true
System.out.println(p1.equals(q)); // prints false
然而,当我们一旦把这个Point类的实例放入到一个容器中问题就出现了:
import java.util.HashSet;
HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);
System.out.println(coll.contains(p2)); // prints false
为什么coll中没有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等价的对象吗?在下面的程序中,我们可以找到其中的一些原因,定义p2a是一个指向p2的对象,但是p2a的类型是Object而非Point类型:
Object p2a = p2;
现在我们重复第一个比较,但是不再使用p2而是p2a,我们将会得到如下的结果:
System.out.println(p1.equals(p2a)); // prints false
到底是那里出了了问题?事实上,之前所给出的equals版本并没有覆盖Object类的equals方法,因为他的类型不同。下面是Object的equals方法的定义
public boolean equals(Object other)
因为Point类中的equals方法使用的是以Point类而非Object类做为参数,因此它并没有覆盖Object中的equals方法。而是一种变化了的重载。在Java中重载被解析为静态的参数类型而非运行期的类型,因此当静态参数类型是Point,Point的equals方法就被调用。然而当静态参数类型是Object时,Object类的equals就被调用。因为这个方法并没有被覆盖,因此它仍然是实现成比较对象标示。这就是为什么虽然p1和p2a具有同样的x,y值,”p1.equals(p2a)”仍然返回了false。这也是会什么HasSet的contains方法返回 false的原因,因为这个方法操作的是泛型,他调用的是一般化的Object上equals方法而非Point类上变化了的重载方法equals
一个更好但不完美的equals方法定义如下:
// A better definition, but still not perfect
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
现在equals有了正确的类型,它使用了一个Object类型的参数和一个返回布尔型的结果。这个方法的实现使用instanceof操作和做了一个造型。它首先检查这个对象是否是一个Point类,如果是,他就比较两个点的坐标并返回结果,否则返回false。
陷阱2:重载了equals的但没有同时重载hashCode的方法
如果你使用上一个定义的Point类进行p1和p2a的反复比较,你都会得到你预期的true的结果。但是如果你将这个类对象放入到HashSet.contains()方法中测试,你就有可能仍然得到false的结果:
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);
System.out.println(coll.contains(p2)); // 打印 false (有可能)
事实上,这个个结果不是100%的false,你也可能有返回ture的经历。如果你得到的结果是true的话,那么你试试其他的坐标值,最终你一定会得到一个在集合中不包含的结果。导致这个结果的原因是Point重载了equals却没有重载hashCode。
注意上面例子的的容器是一个HashSet,这就意味着容器中的元素根据他们的哈希码被被放入到”哈希桶 hash buckets”中。contains方法首先根据哈希码在哈希桶中查找,然后让桶中的所有元素和所给的参数进行比较。现在,虽然最后一个Point类的版本重定义了equals方法,但是它并没有同时重定义hashCode。因此,hashCode仍然是Object类的那个版本,即:所分配对象的一个地址的变换。所以p1和p2的哈希码理所当然的不同了,甚至是即时这两个点的坐标完全相同。不同的哈希码导致他们具有极高的可能性被放入到集合中不同的哈希桶中。contains方法将会去找p2的哈希码对应哈希桶中的匹配元素。但是大多数情况下,p1一定是在另外一个桶中,因此,p2永远找不到p1进行匹配。当然p2和p2也可能偶尔会被放入到一个桶中,在这种情况下,contains的结果就为true了。
最新一个Point类实现的问题是,它的实现违背了作为Object类的定义的hashCode的语义。
如果两个对象根据equals(Object)方法是相等的,那么在这两个对象上调用hashCode方法应该产生同样的值
事实上,在Java中,hashCode和equals需要一起被重定义是众所周知的。此外,hashCode只可以依赖于equals依赖的域来产生值。对于Point这个类来说,下面的的hashCode定义是一个非常合适的定义。
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
这只是hashCode一个可能的实现。x域加上常量41后的结果再乘与41并将结果在加上y域的值。这样做就可以以低成本的运行时间和低成本代码大小得到一个哈希码的合理的分布(译者注:性价比相对较高的做法)。
增加hashCode方法重载修正了定义类似Point类等价性的问题。然而,关于类的等价性仍然有其他的问题点待发现。
陷阱3:建立在会变化字段上的equals定义
让我们在Point类做一个非常微小的变化
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) { // Problematic
this.x = x;
}
public void setY(int y) {
this.y = y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
唯一的不同是x和y域不再是final,并且两个set方法被增加到类中来,并允许客户改变x和y的值。equals和hashCode这个方法的定义现在是基于在这两个会发生变化的域上,因此当他们的域的值改变时,结果也就跟着改变。因此一旦你将这个point对象放入到集合中你将会看到非常神奇的效果。
Point p = new Point(1, 2);
HashSet<Point> coll = new HashSet<Point>();
coll.add(p);
System.out.println(coll.contains(p)); // 打印 true
现在如果你改变p中的一个域,这个集合中还会包含point吗,我们将拭目以待。
p.setX(p.getX() + 1);
System.out.println(coll.contains(p)); // (有可能)打印 false
看起来非常的奇怪。p去那里去了?如果你通过集合的迭代器来检查p是否包含,你将会得到更奇怪的结果。
Iterator<Point> it = coll.iterator();
boolean containedP = false;
while (it.hasNext()) {
Point nextP = it.next();
if (nextP.equals(p)) {
containedP = true;
break;
}
}
System.out.println(containedP); // 打印 true
结果是,集合中不包含p,但是p在集合的元素中!到底发生了什么!当然,所有的这一切都是在x域的修改后才发生的,p最终的的hashCode是在集合coll错误的哈希桶中。即,原始哈希桶不再有其新值对应的哈希码。换句话说,p已经在集合coll的是视野范围之外,虽然他仍然属于coll的元素。
从这个例子所得到的教训是,当equals和hashCode依赖于会变化的状态时,那么就会给用户带来问题。如果这样的对象被放入到集合中,用户必须小心,不要修改这些这些对象所依赖的状态,这是一个小陷阱。如果你需要根据对象当前的状态进行比较的话,你应该不要再重定义equals,应该起其他的方法名字而不是equals。对于我们的Point类的最后的定义,我们最好省略掉hashCode的重载,并将比较的方法名命名为 equalsContents,或其他不同于equals的名字。那么Point将会继承原来默认的equals和hashCode的实现,因此当我们修改了x域后p依然会呆在其原来在容器中应该在位置。
陷阱4:不满足等价关系的equals错误定义
Object中的equals的规范阐述了equals方法必须实现在非null对象上的等价关系:
* 自反原则:对于任何非null值X,表达式x.equals(x)总返回true。
* 等价性:对于任何非空值x和y,那么当且仅当y.equals(x)返回真时,x.equals(y)返回真。
* 传递性:对于任何非空值x,y,和z,如果x.equals(y)返回真,且y.equals(z)也返回真,那么x.equals(z)也应该返回真。
* 一致性:对于非空x,y,多次调用x.equals(y)应该一致的返回真或假。提供给equals方法比较使用的信息不应该包含改过的信息。
* 对于任何非空值x,x.equals(null)应该总返回false.
Point类的equals定义已经被开发成了足够满足equals规范的定义。然而,当考虑到继承的时候,事情就开始变得非常复杂起来。比如说有一个Point的子类ColoredPoint,它比Point多增加了一个类型是Color的color域。假设Color被定义为一个枚举类型:
public enum Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;
}
ColoredPoint重载了equals方法,并考虑到新加入color域,代码如下:
public class ColoredPoint extends Point { // Problem: equals not symmetric
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
return result;
}
}
这是很多程序员都有可能写成的代码。注意在本例中,类ColoredPointed不需要重载hashCode,因为新的ColoredPoint 类上的equals定义,严格的重载了Point上equals的定义。hashCode的规范仍然是有效,如果两个着色点(colored point)相等,其坐标必定相等,因此它的hashCode也保证了具有同样的值。
对于ColoredPoint类自身对象的比较是没有问题的,但是如果使用ColoredPoint和Point混合进行比较就要出现问题。
Point p = new Point(1, 2);
ColoredPoint cp = new ColoredPoint(1, 2, Color.RED);
System.out.println(p.equals(cp)); // 打印真 true
System.out.println(cp.equals(p)); // 打印假 false
“p等价于cp”的比较这个调用的是定义在Point类上的equals方法。这个方法只考虑两个点的坐标。因此比较返回真。在另外一方面,“cp 等价于p”的比较这个调用的是定义在ColoredPoint类上的equals方法,返回的结果却是false,这是因为p不是 ColoredPoint,所以equals这个定义违背了对称性。
违背对称性对于集合来说将导致不可以预期的后果,例如:
Set<Point> hashSet1 = new java.util.HashSet<Point>();
hashSet1.add(p);
System.out.println(hashSet1.contains(cp)); // 打印 false
Set<Point> hashSet2 = new java.util.HashSet<Point>();
hashSet2.add(cp);
System.out.println(hashSet2.contains(p)); // 打印 true
因此虽然p和cp是等价的,但是contains测试中一个返回成功,另外一个却返回失败。
你如何修改equals的定义,才能使得这个方法满足对称性?本质上说有两种方法,你可以使得这种关系变得更一般化或更严格。更一般化的意思是这一对对象,a和b,被用于进行对比,无论是a比b还是b比a 都返回true,下面是代码:
public class ColoredPoint extends Point { // Problem: equals not transitive
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
else if (other instanceof Point) {
Point that = (Point) other;
result = that.equals(this);
}
return result;
}
}
在ColoredPoint中的equals的新定义比老定义中检查了更多的情况:如果对象是一个Point对象而不是ColoredPoint,方法就转变为Point类的equals方法调用。这个所希望达到的效果就是equals的对称性,不管”cp.equals(p)”还是”p.equals(cp)”的结果都是true。然而这种方法,equals的规范还是被破坏了,现在的问题是这个新等价性不满足传递性。考虑下面的一段代码实例,定义了一个点和这个点上上两种不同颜色点:
ColoredPoint redP = new ColoredPoint(1, 2, Color.RED);
ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);
redP等价于p,p等价于blueP
System.out.println(redP.equals(p)); // prints true
System.out.println(p.equals(blueP)); // prints true
然而,对比redP和blueP的结果是false:
System.out.println(redP.equals(blueP)); // 打印 false
因此,equals的传递性就被违背了。
使equals的关系更一般化似乎会将我们带入到死胡同。我们应该采用更严格化的方法。一种更严格化的equals方法是认为不同类的对象是不同的。这个可以通过修改Point类和ColoredPoint类的equals方法来达到。你能增加额外的比较来检查是否运行态的这个Point类和那个Point类是同一个类,就像如下所示的代码一样:
// A technically valid, but unsatisfying, equals method
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY()
&& this.getClass().equals(that.getClass()));
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
你现在可以将ColoredPoint类的equals实现用回刚才那个不满足对称性要的equals实现了。
public class ColoredPoint extends Point { // 不再违反对称性需求
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
return result;
}
}
这里,Point类的实例只有当和另外一个对象是同样类,并且有同样的坐标时候,他们才被认为是相等的,即意味着 .getClass()返回的是同样的值。这个新定义的等价关系满足了对称性和传递性因为对于比较对象是不同的类时结果总是false。所以着色点 (colored point)永远不会等于点(point)。通常这看起来非常合理,但是这里也存在着另外一种争论——这样的比较过于严格了。
考虑我们如下这种稍微的迂回的方式来定义我们的坐标点(1,2)
Point pAnon = new Point(1, 1) {
@Override public int getY() {
return 2;
}
};
pAnon等于p吗?答案是假,因为p和pAnon的java.lang.Class对象不同。p是Point,而pAnon是Point的一个匿名派生类。但是,非常清晰的是pAnon的确是在坐标1,2上的另外一个点。所以将他们认为是不同的点是没有理由的。
canEqual 方法
到此,我们看其来似乎是遇到阻碍了,存在着一种正常的方式不仅可以在不同类继承层次上定义等价性,并且保证其等价的规范性吗?事实上,的确存在这样的一种方法,但是这就要求除了重定义equals和hashCode外还要另外的定义一个方法。基本思路就是在重载equals(和hashCode)的同时,它应该也要要明确的声明这个类的对象永远不等价于其他的实现了不同等价方法的超类的对象。为了达到这个目标,我们对每一个重载了equals的类新增一个方法canEqual方法。这个方法的方法签名是:
public boolean canEqual(Object other)
如果other 对象是canEquals(重)定义那个类的实例时,那么这个方法应该返回真,否则返回false。这个方法由equals方法调用,并保证了两个对象是可以相互比较的。下面Point类的新的也是最终的实现:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result =(that.canEqual(this) && this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
public boolean canEqual(Object other) {
return (other instanceof Point);
}
}
这个版本的Point类的equals方法中包含了一个额外的需求,通过canEquals方法来决定另外一个对象是否是是满足可以比较的对象。在Point中的canEqual宣称了所有的Point类实例都能被比较。
下面是ColoredPoint相应的实现
public class ColoredPoint extends Point { // 不再违背对称性
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (that.canEqual(this) && this.color.equals(that.color) && super.equals(that));
}
return result;
}
@Override public int hashCode() {
return (41 * super.hashCode() + color.hashCode());
}
@Override public boolean canEqual(Object other) {
return (other instanceof ColoredPoint);
}
}
在上显示的新版本的Point类和ColoredPoint类定义保证了等价的规范。等价是对称和可传递的。比较一个Point和 ColoredPoint类总是返回false。因为点p和着色点cp,“p.equals(cp)返回的是假。并且,因为cp.canEqual(p) 总返回false。相反的比较,cp.equals(p)同样也返回false,由于p不是一个ColoredPoint,所以在 ColoredPoint的equals方法体内的第一个instanceof检查就失败了。
另外一个方面,不同的Point子类的实例却是可以比较的,同样没有重定义等价性方法的类也是可以比较的。对于这个新类的定义,p和pAnon的比较将总返回true。下面是一些例子:
Point p = new Point(1, 2);
ColoredPoint cp = new ColoredPoint(1, 2, Color.INDIGO);
Point pAnon = new Point(1, 1) {
@Override public int getY() {
return 2;
}
};
Set<Point> coll = new java.util.HashSet<Point>();
coll.add(p);
System.out.println(coll.contains(p)); // 打印 true
System.out.println(coll.contains(cp)); // 打印 false
System.out.println(coll.contains(pAnon)); // 打印 true
这些例子显示了如果父类在equals的实现定义并调用了canEquals,那么开发人员实现的子类就能决定这个子类是否可以和它父类的实例进行比较。例如ColoredPoint,因为它以”一个着色点永远不可以等于普通不带颜色的点重载了” canEqual,所以他们就不能比较。但是因为pAnon引用的匿名子类没有重载canEqual,因此它的实例就可以和Point的实例进行对比。
canEqual方法的一个潜在的争论是它是否违背了Liskov替换准则(LSP)。例如,通过比较运行态的类来实现的比较技术(译者注: canEqual的前一版本,使用.getClass()的那个版本),将导致不能定义出一个子类,这个子类的实例可以和其父类进行比较,因此就违背了 LSP。这是因为,LSP原则是这样的,在任何你能使用父类的地方你都可以使用子类去替换它。在之前例子中,虽然cp的x,y坐标匹配那些在集合中的点,然而”coll.contains(cp)”仍然返回false,这看起来似乎违背得了LSP准则,因为你不能这里能使用Point的地方使用一个 ColoredPointed。但是我们认为这种解释是错误的,因为LSP原则并没有要求子类和父类的行为一致,而仅要求其行为能一种方式满足父类的规范。
通过比较运行态的类来编写equals方法(译者注: canEqual的前一版本,使用.getClass()的那个版本)的问题并不是违背LSP准则的问题,但是它也没有为你指明一种创建派生类的实例能和父类实例进行对比的的方法。例如,我们使用这种运行态比较的技术在之前的”coll.contains(pAnon)”将会返回false,并且这并不是我们希望的。相反我们希望“coll.contains(cp)”返回false,因为通过在ColoredPoint中重载的equals,我基本上可以说,一个在坐标1,2上着色点和一个坐标1,2上的普通点并不是一回事。然而,在最后的例子中,我们能传递Point两种不同的子类实例到集合中 contains方法,并且我们能得到两个不同的答案,并且这两个答案都正确。
•使用字符串缓冲区而不是字符串连接,当进行连续字符串操作时要避免不必要地创建那些最终必须经过垃圾回收的对象。
•避免连续写入 Java 控制台以减少字符串操作、文本格式化以及输出的花费。
•必要时通过使用变量的原语类型来避免对象创建和操作的花费。
•经常高速缓存用过的对象以减少必须的垃圾回收数量,并且避免重复创建对象的需求。
•尽可能分组本地操作以减少 Java 本地接口(JNI)的调用。
•只有在必要时再使用同步方法,以此限制在 JVM 和操作系统中的多任务。
•除非必要避免调用垃圾回收器。如果您必须调用它,只有在空闲时间或一些非关键阶段再这样做。
•可能时使用整型而不是长整型,因为 32 位操作快于 64 位。
•可能时申明方法为 final。JVM 处理 final 的方法较好。
•当创建常量时为了减少变量需要初始化的次数,使用关键字 static final。
•避免不必要的“casts”和“instanceof”引用,因为在 Java 中销毁操作不是在编译时而是在运行时执行的。
•当数组可以满足要求时尽可能避免使用向量。
•从向量末端添加和删除项以得到较高性能。
•避免在循环中分配对象。
•使用缓冲区 I/O 并调优缓冲区大小。
•使用连接池和准备缓存声明进行数据库访问。
•使用连接池连接到数据库并重用连接而不是重复打开和关闭连接。
•最大化线程生存期并最小化线程创建和销毁循环。
•最小化共享资源的争用。
•最小化短生存期对象的创建。
•避免远程方法调用。
•使用回调以避免阻塞远程方法调用。
•避免创建只用来访问一个方法的对象。
•尽可能保持同步方法处于循环外。
•在数据库中以 Unicode 形式存储字符串和字符数据。
•记录 CLASSPATH 以便最常用的库先出现。
Analytics Plugin created by Web Hosting