Java包、类的加载机制(理论)

本人Java菜鸡一枚,如果下面的内容有误,请各位拼命留言,我秒秒钟改!

做项目的同时努力补全知识点或许是当务之急?

问题

此面试问题暴露了我对Java内部原理的无知,哈哈,赶紧学。题外话:换做面C++的话,我估计面试官会用一个多继承里头的问题来考。

有a.jar和b.jar,里面都有某个类MyClass且他们的包名完全相同,但是有不同的foo()方法,在一个项目中同时引入这两个jar会出现什么奇妙的事情呢?

这个问题的答案是,要根据类加载器的加载顺序来决定我们构建MyClass对象的时候究竟会先找到哪个jar里头的文件,a.jar/b.jar哪个运气好拔得头筹了,剩下的就被忽视了。

如果我们构建了不同的类加载器对象的话,可以通过自定义的类加载器来同时加载这两个长得看起来一模一样的类,并且可以各自调用其方法属性什么的。

更详尽的回答可以看这里:http://stackoverflow.com/questions/6879652/possible-to-use-two-java-classes-with-same-name-and-same-package

包(Package)、编译单元

C/C++里面用 #include来表示引用的类(文件),并且用过C++的盆友肯定知道 namespace 这个关键字可以用来隔离名字相同东西,最常见的莫过于 using namespace std; 了。以上这些与所谓“访问权限控制”有关(详见《Java编程思想》第6章)。

而Java以及诸如Python这种语言采用包(package)作为库单元,一个包内包含一组类,比如ArrayList在Java标准库中被放置在java.util包(名字空间)下。包的作用有点像地址,在寻找一个类的时候就像快递分拣一样,根据包的名字能找到最终的目标,一个不恰当的例子就是,package 浙江.杭州.西湖 底下可能会有 同仁医院 这个类,而 package 福建.福州.鼓楼(这个例子不恰当,因为按照Java的原意,包名应该是“从后往前”写的,如cn.com.ibm) 底下也可以有 同仁医院 这个类,在Java代码中使用类的内容的时候,Java虚拟机就得根据包的名称来确定你要引用的到底是哪个医院类了。

而所谓编译单元,大多是指Java源代码文件。一个编译单元的后缀就是.java,文件里只能有一个public的类且该类的名称与文件名相同,否则就会导致编译错误。Java解释器的运行过程大致是:找出环境变量CLASSPATH(包含一个或多个目录)用作查找.class文件的根目录。从根目录开始,解释器获取包的名称并将每个句点替换为反斜杠\,比如 浙江.杭州.西湖 就变成了目录 浙江\杭州\西湖(根据操作系统可能略有不同,变为正斜杠/也是有可能的)。随后解释器就在这么个目录中查找对应你给的类名称的.class文件,比如上面的 同仁医院.class

编译执行流程

估计大家都知道,Java中的类可以被动态加载到Java虚拟机中执行。下面这张图(推荐https://www.processon.com/,在线画简单流程图的好东西)可以大致说明:

Java Classloader

Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance() 方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。上图可以看出一点,就是通过不同的类加载器,可以加载出不同的Class类的实例。

java.lang.ClassLoader

之前说过类加载器是干啥使的,除了加载类以外,其实诸如图像文件什么的资源也是它加载的,不过这里暂且不谈。

java.lang.ClassLoader其实是一个抽象类,有几个比较重要的方法:

  • getParent() 用于返回该类加载器的父类加载器;这个方法与经常能看到的“双亲委派”这个关键词有关:)
  • loadClass(String name) 加载名称为name的类,返回java.lang.Class实例。
    该方法有三步:
    1.调用findLoadedClass方法看看是不是已经加载过这个类了;
    2.如果没有,在父类加载器上调用loadClass方法。如果父类加载器是null(啊..到底了…),则用虚拟机的内置类加载器(下面讲到的引导类加载器);
    3.还是不行,调用findClass方法查找类
  • findClass(String name) 查找名称为name的类,返回java.lang.Class实例
  • findLoadedClass(String name) 查找一个名为name的已经加载过的类,返回java.lang.Class实例
  • defineClass(String name, byte[] b, int off, int len) 把字节数组b中的内容转换为Java类,返回java.lang.Class实例,这是个final方法
  • resolveClass(Class<?> c) 链接指定的Java类

注意:里面的参数name都是二进制名称。

系统提供的类加载器主要有三类:

  • Bootstrap class loader: 引导类加载器,用原生代码写的,用途就是bootstrap Java的核心库。
  • Extensions class loader: 扩展类加载器,用来加载扩展库。
  • System class loader: 系统类加载器,根据Java应用类的路径(CLASSPATH)加载Java类,一般我们写的东西就是它来加载的。

除了写好的类加载器外,我们可以自己继承java.lang.ClassLoader来实现自己的类加载器,满足一些奇奇怪怪的需求,比如问题1…

getParent()——找“爹”方法

在参考文献中是这么写的“对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器”,是不是很拗口?比如我们自己继承了个ClassLoader类的话(假设他叫MyClassLoader),一般会在这个方法里面返回System class loader,为啥呢?因为我们自己写的MyClassLoader也是个类(先有鸡还是先有蛋,哈哈),也需要一个加载器把MyClassLoader加载了,我们才能用它去加载别的东西。

有了这个方法,就可以在加载类的时候有一个向上追溯的找类过程,这个感觉就有点像JavaScript里面的XX链(原型链、作用域链……哦,扯远了)。类加载器之间可以形成树形结构,比如下面这样子(图版权归IBM所有):

类加载器的树状结构

一路都可以getParent()上去,有些JDK可能输出不到最初的那个引导类加载器(AppClassLoader的getParent()返回了null)。

类加载器的代理模式——”我”是”我”?

Java虚拟机怎么判断两个类相同呢?通过包名+类名?不是的,还得看加载这个类的类加载器是不是一样。本文的第一张图可以看出,相同的字节码,被不同的类加载器加载之后,得到的Class类实例是不同的。

不同的类加载器使得包、类名称完全相同的两个类可以并存在Java虚拟机中,且不同加载器加载的类之间是不兼容的。代理模式的设计动机是保证Java核心库的类型安全。我们知道java.lang.Object类是”万物的基础“,如过这个东西能够用自己的类加载器来先加载的话,那就可能存在多个版本的java.lang.Object类并且他们还互不兼容,这就懵逼了。

加载类的过程

首先明确一点,由于类的加载是自顶向下尝试的,真正完成类加载工作的加载器和启动加载过程的加载器有可能不是同一个。

defineClass方法的作用是”真正的“完成类的加载过程(你看它的传入参数,字节数组什么的,多么朴实),执行这个方法的加载器被称作定义加载器;启动类的加载过程是调用loadClass方法实现的(它只要给个名字就行),执行这个方法的加载器叫做初始加载器,它要找不到类的话,就抛出个ClassNotFoundException异常。成功加载一个类后,会把得到的Class类实例缓存起来,下次请求时就不会重复加载了,即对于一个类(全名表示)来说,loadClass方法不会在一个classLoader里头重复调用。

 

额,如果有空的话,下一篇会实作一个自定义的ClassLoader。

参考文献

  • 彻底搞懂Java Classloader: http://weli.iteye.com/blog/1682625
  • 深入探讨Java类加载器:https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
  • 《Java编程思想(第四版)》

在SpringMVC 4 中用注释(Annotation)方式使用 Google Kaptcha (Captcha)

原创内容,转载请注明来自http://boweihe.me/?p=2026

近期在努力学习SpringMVC,因为之前对JSP也是一知半解的,干脆拿了本《Spring in Action》(4th edition)啃,发现使用注释的方式比用xml来的有意思一些。网站中要用到验证码,目前能找到的文档都是用xml配置的,感觉有点不爽,决定学我党“摸着石头过河”一次,希望不要naive了..

Maven库

Google自家的库估计是没了,大概是时间太久远了吧。我用的Maven库是这个

Bean配置文件

需要把原有的applicationContext.xml用注释的方式实现,其实就是让Spring找到个可以用的Bean并加载相关配置。

 

我构造了一个类来搞定这个事情,里面的配置参数会从application.properties文件中读取,我仅仅实现了我需要的几个参数,反正如果要加的话就在kaptchaProperties方法里面写就可以,然后Autowire一个Environment用来读取文件配置参数。主要是 @Configuration 注释,这个是告诉Spring我是个配置类,这个还是从Hibernate配置中学过来的,哈哈。

声明出来的这个 @Bean(name = "captchaProducer") 就是后面Controller里面要用的了

对应的application.properties,具体含义可以看参考文献里,解释的很清楚了。

CaptchaController 控制器

项目在实习前怕是赶不完来了,我还是少花点时间写博客吧。

控制器的实现Wiki上都有,主要的是要@Autowire之前我们做好的那个Bean,大概是这样。其实就是个基于set方法的注入嘛~

 

参考文献:

  1. 在springmvc项目中使用kaptcha生成验证码
  2. 简单Maven的Web项目之验证码(Kaptcha篇)
  3. Spring mvc框架下使用kaptcha生成验证码
  4. https://code.google.com/archive/p/kaptcha/wikis

 

[Java]计算组合

基本功不扎实,写了半天写出来。

输入:一个长度n数组,比如{0,1,3,5},以及m

输出:组合CNM

===============

Java中HashMap遍历的两种方式

原文地址: http://www.javaweb.cc/language/java/032291.shtml

第一种:
  

  效率高,以后一定要使用此种方式!
第二种:
  

  效率低,以后尽量少使用!

HashMap的遍历有两种常用的方法,那就是使用keyset及entryset来进行遍历,但两者的遍历速度是有差别的,下面请看实例:
  public class HashMapTest {
  public static void main(String[] args) …{
  HashMap hashmap = new HashMap();
  for (int i = 0; i < 1000; i ) …{
  hashmap.put(“” i, “thanks”);
  }
  long bs = Calendar.getInstance().getTimeInMillis();
  Iterator iterator = hashmap.keySet().iterator();
  while (iterator.hasNext()) …{
  System.out.print(hashmap.get(iterator.next()));
  }
  System.out.println();
  System.out.println(Calendar.getInstance().getTimeInMillis() – bs);
  listHashMap();
  }
  public static void listHashMap() …{
  java.util.HashMap hashmap = new java.util.HashMap();
  for (int i = 0; i < 1000; i ) …{
  hashmap.put(“” i, “thanks”);
  }
  long bs = Calendar.getInstance().getTimeInMillis();
  java.util.Iterator it = hashmap.entrySet().iterator();
  while (it.hasNext()) …{
  java.util.Map.Entry entry = (java.util.Map.Entry) it.next();
  // entry.getKey() 返回与此项对应的键
  // entry.getValue() 返回与此项对应的值
  System.out.print(entry.getValue());
  }
  System.out.println();
  System.out.println(Calendar.getInstance().getTimeInMillis() – bs);
  }
  }
  对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entryset只是遍历了第一次,他把key和value都放到了entry中,所以就快了。

Java中HashMap遍历的两种方式(本教程仅供研究和学习,不代表JAVA中文网观点)
本篇文章链接地址:http://www.javaweb.cc/language/java/032291.shtml
如需转载请注明出自JAVA中文网:http://www.javaweb.cc/

Eclipse JRI : Java 调用 R 报错: Cannot find JRI native library!

Q: I get the following error, what’s wrong?
java.lang.UnsatisfiedLinkError: no jri in java.library.path
A: Usually it means that you didn’t setup the necessary environment variables properly or the JRI library is not where it is expected to be. The recommended way to start JRI programs is to use the run script which is generated along with the library. It sets everything up and is tested to work. If you want to write your own script or launcher, you must observe at least the following points:

  • R_HOME must be set correctly
  • (Windows): The directory containing R.dll must be in your PATH
  • (Mac): Well, it’s a Mac, so it just works ;).
  • (unix): R must be compiled using --enable-R-shlib and the directory containing libR.so must be in LD_LIBRARY_PATH. Also libjvm.so and other dependent Java libraries must be on LD_LIBRARY_PATH.
  • JRI library must be in the current directory or any directory listed in java.library.path. Alternatively you can specify its path with 
    -Djava.library.path= when starting the JVM. When you use the latter, make sure you check java.library.path property first such that you won’t break your Java.
  • Depending on your system, the R verison and other features you want to use, you may have to set additional settings such as R_SHARE_DIRR_INCLUDE_DIR andR_DOC_DIR.

Again, I don’t think you want to worry about all of the above – just use the start script!

特别注意红字标注的这一行,

QQ截图20130728131711

在这边修改实现即可。

参考:http://www.rforge.net/JRI/