java基础的,随便记录

来源:互联网 发布:淘宝电商运营必备表格 编辑:程序博客网 时间:2024/04/29 15:51

而具有相等的hashcode的两个对象equals不一定成立


仅仅实现hashcode方法是不够的,hashcode只用于实现查找hash地址。
还得实现equals方法,equals方法严格判断两个对象是否相等

首先了解散列:
散列的目的:使用一个对象来查找另一个对象。
list
set:两个元素不重复应该依据什么来判断?这就是object.equals方法了,
但是每增加一个元素就检查一次,当元素很多的时,后添加到集合中的元素比较的次数就多了。
也就是说,如果集合中已经有1000个元素,那么第1001个元素加入集合前要进行1000次比较,大大降低了效率。
于是java采用了哈希表的原理。哈希(hash)算法也称为散列算法,是将数据依特定算法直接指定到一个地址上,
初学可以这样理解:hashCode方法实际上返回的是对象存储的物理地址。
这样一来,当集合需要添加新元素时,先调用这个元素的hashcode方法,一下子定位他应该在的物理位置上。
如果位置上没有元素,则可直接存储在这个位置上,不再进行任何比较。
如果位置上有元素,就调用他的equals方法与新元素比较。相同的话就不存了,不相同就散列到其它位置。

结论:
java对于equals()方法和hashcode()方法是这样规定的:
1.如果两个对象相同【equals】,那么他们的hashcode值一定相同
2.如果两个对象的hashcode相同,他们并不一定相同【equals】,
3.如果两个对象不相同,那么他们的hashcode值可能相同。【比如下面图中的9,57,89】

下面是非常直观的了解(重点是了解hash算法)【其实非常简单】



判断两对象是否相等的一般规则:
1)。判断两个对象的hashcode是否相等
如果不等则认为两对象不等,完毕
如果相等,装入2)
2),判断两个对象用equals运算是否相等
如果不等,则认为两个对象也不等
如果相等,认为两个对象相等(equals是判断两个对象相等的关键)//可以重写eqlals方法

总结:
1.“==”用来比较两个变量的地址
2.“equals”方法如果没有重写则和“==”等价,如果重新了则按照重写来比较
3.hashcode返回对象的在内存地址中的一个int值,(如果没有重新,则任何对象的hashCode都不等);
4.equals()相等的对象,hashcode一定相等,‘ 最好在重写equals()方法的时候重写hashcode();
hashcode()不等,一定能推出equals()不等
=============================================================================

session 和cookie



=============================有关String ,StrringBuffer,Stringbuilder==========================

String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)

简要的说, String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。
而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
String S1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个
String S1 = “This is only a” + “ simple” + “test”; 其实就是:
String S1 = “This is only a simple test”; 所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 +S3 + S4;
这时候 JVM 会规规矩矩的按照原来的方式去做

在大部分情况下 StringBuffer > String
StringBuffer
Java.lang.StringBuffer线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。
可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append("le") 会使字符串缓冲区包含“startle”,而 z.insert(4, "le") 将更改字符串缓冲区,使之包含“starlet”。
在大部分情况下 StringBuilder > StringBuffer
java.lang.StringBuilde
java.lang.StringBuilder一个可变的字符序列是5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。

有关map遍历的一些写法:



java实现的文件下载:
分两种:
一种是本地下载(也就是文件和服务在同一台机器上),
一种是远程下载(文件在单独的文件服务器上)
下面是二者的对比:



第二中远程下载的代码有点问题,下面重新做了修改
具体代码如下:本地下载:
    String workPath = request.getParameter("workPath");    String filename=workPath.substring(workPath.lastIndexOf("/")+1);    StringBuffer s = new StringBuffer(fileRoot);    //得到Web模块在服务器上的路径    s.append(workPath);    //本地测试用下面[记住一点,文件必须在提供down服务的机器上,说白了就是服务必须和文件在同一台机器上]//   StringBuffer s = new StringBuffer("G:\\KuGou\\LYn - Thank You My Dear.mp3");    System.out.println(s);    File file = new File(s.toString());    FileInputStream fis = null ;    if (file.exists()) {    //如果文件存在这执行文件的读写操作    System.out.println("文件实际长度" + file.length());    fis = new FileInputStream(file);         DataOutputStream oos = new DataOutputStream(response.            getOutputStream());    //注意下面文件头的设置    response.setHeader( "Content-Disposition", "attachment;filename="  + new String( filename.getBytes("gb2312"), "ISO8859-1" ) );    //对于文件名是纯简体中文的文件可以使用new String( filename.getBytes("gb2312"), "ISO8859-1" )方法将filename字符串转化成ISO8859-1    //如果中间还有繁体字的话,就需要使用其他的方法来解决中文显示的问题    response.setContentType("application/x-msdownload");    //设置文件内容的类型    int cache_length=1024;byte[] filebyte = new byte[cache_length];    for (int k = 0; k < cache_length; k++) {        filebyte[k] = 0;    }    int i = fis.read(filebyte);    //如果i=-1就说明文件已经到头了,不要再读文件    while (i != -1) {        oos.write(filebyte);        for (int k = 0; k < cache_length; k++) {            filebyte[k] = 0;        }        i = fis.read(filebyte);    }     oos.write(filebyte);             }  fis.close();    

远程下载:

<pre name="code" class="java">    public void doGet(HttpServletRequest request, HttpServletResponse response) throws  ServletException, IOException {    String workPath = request.getParameter("workPath");    String filename=workPath.substring(workPath.lastIndexOf("/")+1);    if(!fileRoot.endsWith("/")){    fileRoot = fileRoot +"/";    };    if(workPath.startsWith("/")){    workPath = workPath.substring(1);//把前面的/去掉    }    StringBuffer s = new StringBuffer(fileRoot);    //得到Web模块在服务器上的路径    s.append(workPath);    URL url = new URL(s.toString());    HttpURLConnection httpUrl = (HttpURLConnection) url.openConnection();    //连接指定的网络资源    httpUrl.connect();    //获取网络输入流    BufferedInputStream  bis = new BufferedInputStream(httpUrl.getInputStream());    BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());    //注意下面文件头的设置    response.setHeader( "Content-Disposition", "attachment;filename="  + new String( filename.getBytes("gb2312"), "ISO8859-1" ) );    //对于文件名是纯简体中文的文件可以使用new String( filename.getBytes("gb2312"), "ISO8859-1" )方法将filename字符串转化成ISO8859-1    //如果中间还有繁体字的话,就需要使用其他的方法来解决中文显示的问题     response.setContentType(contentType);    //设置文件内容的类型    int cache_length=1024;byte[] filebyte = new byte[cache_length];int len ;while((len=bis.read(filebyte))!=-1){bos.write(filebyte,0,len);}  bos.flush();//刷新输入流并强制写出所有缓冲的输出字符  bis.close();  bos.close();        }


java实现上传功能
用到的包:import org.apache.commons.fileupload.DiskFileUpload;
web.xml文件中配置如下:
 <servlet>    <servlet-name>FileUploadSlt</servlet-name>    <servlet-class>com.jt.xiaoyang.web.XyFileUploadSlt</servlet-class>    <init-param>        <param-name>uploadPath</param-name><!-- 上传的真实路径,部署服务器的时候该为服务器的保存路径 -->        <param-value>G://files</param-value>    </init-param>    <init-param>        <param-name>uploadPathUrl</param-name><!-- 返回给客户端的路径,【这里做了tomcat文件路径映射】 -->        <param-value>http://localhost:8080/files</param-value>    </init-param>  </servlet>  <servlet-mapping>    <servlet-name>FileUploadSlt</servlet-name>    <url-pattern>/upload</url-pattern>  </servlet-mapping>




//先上传到临时文件夹中
public void init(ServletConfig config) throws ServletException {uploadPath = config.getInitParameter("uploadPath");uploadPathUrl= config.getInitParameter("uploadPathUrl");tempPath = uploadPath + "/_temp";}
protected void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {Map<String, String> params=new HashMap<String, String>();try {DiskFileUpload fu = new DiskFileUpload();// 设置最大文件尺寸,这里是4MB//fu.setSizeMax(4194304);// 设置缓冲区大小,这里是4kbfu.setSizeThreshold(4096);// 设置临时目录:fu.setRepositoryPath(tempPath);// 得到所有的文件:List fileItems = fu.parseRequest(request);Iterator i = fileItems.iterator();// 依次处理每一个文件:while (i.hasNext()) {FileItem fi = (FileItem) i.next();if(fi.isFormField()){params.put(fi.getFieldName(), fi.getString());} else {// 获得文件名,这个文件名包括路径:String fileExtName = FileUtil.getFileExtName(fi.getName());String targetFileName = StringUtil.getRandomString(8) + "."+ fileExtName;params.put("targetFileName", targetFileName);//先写入临时目录params.put("tempPath", tempPath);fi.write(new File(tempPath+"/" + targetFileName));}}} catch (Exception e) {// 可以跳转出错页面e.printStackTrace();}//保存成功saveDone(response, params);}
protected void saveDone(HttpServletResponse response,Map<String,String> params) {//复制文件到目标目录String targetUri=getTargetUri(params);String tempPath=params.get("tempPath");String targetFileName=params.get("targetFileName");
下面是自己写的工具类,把刚上传到服务器临时文件夹中文件,拷贝一份到自己想要的目录中
 FileUtil.copyFile(new File(tempPath+"/" + targetFileName), new File(uploadPath+"/"+targetUri+"/" + targetFileName));
// 返回上传文件的urltry {response.getWriter().write(uploadPathUrl+"/"+targetUri+"/"+ targetFileName);} catch (IOException e) {e.printStackTrace();}}protected String getTargetUri(Map<String, String> params){//按日期分目录String targetUri=DateUtil.convertDateToString(new Date());try {FileUtil.buildDirectory(uploadPath+"/"+targetUri);} catch (IOException e) {e.printStackTrace();}return targetUri;}


以上是服务:
下面是对他的调用:
调用方法1:可以直接在页面中写
<form  action='/upload'>
<input type="file" name="files[]" multiple="multiple" title='Click to add Files'>
</form>

调用方法2:插件调用:[源码放再资源里!]jquery多文件上传




============================java对日期的一些操作===========================================================


JAVA获取当前日期

import java.util.*;    
  
public class D    
{    
public static void main(String []abc)    
{    
int y,m,d,h,mi,s;    
Calendar cal=Calendar.getInstance();    
y=cal.get(Calendar.YEAR);    
m=cal.get(Calendar.MONTH);    //注意这里获取的月份比真实的少1,应该加上1才是真实的当前月份
d=cal.get(Calendar.DATE);    
h=cal.get(Calendar.HOUR_OF_DAY);    
mi=cal.get(Calendar.MINUTE);    
s=cal.get(Calendar.SECOND);    
System.out.println("现在时刻是"+y+"年"+m+"月"+d+"日"+h+"时"+mi+"分"+s+"秒");    
}    
  
}   
##########################################################################    
public class Main{    
    public static void main(String[] args){    
        java.util.Calendar c=java.util.Calendar.getInstance();    
        java.text.SimpleDateFormat f=new java.text.SimpleDateFormat("yyyy年MM月dd日hh时mm分ss秒");    
        System.out.println(f.format(c.getTime()));    
    }    
}  


##########################################################################   

public String GetNowDate(){   
    String temp_str="";   
    Date dt = new Date();   
    //最后的aa表示“上午”或“下午”    HH表示24小时制    如果换成hh表示12小时制   
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss aa");   
    temp_str=sdf.format(dt);   
    return temp_str;   

java获取明天:

Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.add(calendar.DATE, 1);//明天 -1是昨天,以此类推
date = calendar.getTime();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String tomorrowStr = dateFormat.format(date);

====================================java获取根据用户ip获取用户地址===================================================
解决策略:用淘宝的服务:

过程:
刚开始打算直接在页面有js调用,

jQuery(document).ready(function() {var urlStr = "http://ip.taobao.com/service/getIpInfo.php?ip="+'${userProperty.lastLoginIp}';  $.post(urlStr,function(data){alert(data);});});



可是提示报错
已阻止交叉源请求:同源策略不允许读取 http://ip.taobao.com/service/getIpInfo.php?ip=%E4%B8%AD%E5%9B%BD%20%20%E5%8C%97%E4%BA%AC%E5%B8%82%20%20%E5%8C%97%E4%BA%AC%E5%B8%82%20%20%20%20%E7%94%B5%E4%BF%A1 上的远程资源。可以将资源移动到相同的域名上或者启用 CORS 来解决这个问题。
发现在js中不能调用别的源的请求,如果

于是放弃页面操作,该为后台获取代码如下

import java.io.BufferedReader;import java.io.DataOutputStream;import java.io.IOException;import java.io.InputStreamReader;import java.io.UnsupportedEncodingException;import java.net.HttpURLConnection;import java.net.URL;/** *  根据IP地址获取详细的地域信息 *  @project:personGocheck *  @class:AddressUtils.java */public class IPUtils { public static String getAddress(String ip) throws UnsupportedEncodingException{return getAddresses("ip="+ip, "utf-8");}; /**  * @param content  *            请求的参数 格式为:name=xxx&pwd=xxx  * @param encoding  *            服务器端请求编码。如GBK,UTF-8等  * @return  * @throws UnsupportedEncodingException  */ public static String getAddresses(String content, String encodingString) throws UnsupportedEncodingException {  // 这里调用pconline的接口  String urlStr = "http://ip.taobao.com/service/getIpInfo.php";  // 从http://whois.pconline.com.cn取得IP所在的省市区信息  String returnStr = getResult(urlStr, content, encodingString);  if (returnStr != null) {   // 处理返回的省市区信息   System.out.println(returnStr);   String[] temp = returnStr.split(",");   if(temp.length<3){    return "0";//无效IP,局域网测试   }   String region = (temp[5].split(":"))[1].replaceAll("\"", "");   region = decodeUnicode(region);// 省份String country = "";String area = "";// String region = "";String city = "";String county = "";String isp = "";for (int i = 0; i < temp.length; i++) {switch (i) {case 1:country = (temp[i].split(":"))[2].replaceAll("\"", "");country = decodeUnicode(country);// 国家break;case 3:area = (temp[i].split(":"))[1].replaceAll("\"", "");area = decodeUnicode(area);// 地区 break;case 5:region = (temp[i].split(":"))[1].replaceAll("\"", "");region = decodeUnicode(region);// 省份 break; case 7:city = (temp[i].split(":"))[1].replaceAll("\"", "");city = decodeUnicode(city);// 市区break; case 9:county = (temp[i].split(":"))[1].replaceAll("\"", "");county = decodeUnicode(county);// 地区 break;case 11:isp = (temp[i].split(":"))[1].replaceAll("\"", "");isp = decodeUnicode(isp); // ISP公司break;}}       System.out.println(country+/*" "+area+*/"  "+region+"  "+city+"  "+county+"  "+isp);   return country+/*" "+area+*/"  "+region+"  "+city+"  "+county+"  "+isp;  }  return null; } /**  * @param urlStr  *            请求的地址  * @param content  *            请求的参数 格式为:name=xxx&pwd=xxx  * @param encoding  *            服务器端请求编码。如GBK,UTF-8等  * @return  */ private static  String getResult(String urlStr, String content, String encoding) {  URL url = null;  HttpURLConnection connection = null;  try {   url = new URL(urlStr);   connection = (HttpURLConnection) url.openConnection();// 新建连接实例   connection.setConnectTimeout(2000);// 设置连接超时时间,单位毫秒   connection.setReadTimeout(2000);// 设置读取数据超时时间,单位毫秒   connection.setDoOutput(true);// 是否打开输出流 true|false   connection.setDoInput(true);// 是否打开输入流true|false   connection.setRequestMethod("POST");// 提交方法POST|GET   connection.setUseCaches(false);// 是否缓存true|false   connection.connect();// 打开连接端口   DataOutputStream out = new DataOutputStream(connection     .getOutputStream());// 打开输出流往对端服务器写数据   out.writeBytes(content);// 写数据,也就是提交你的表单 name=xxx&pwd=xxx   out.flush();// 刷新   out.close();// 关闭输出流   BufferedReader reader = new BufferedReader(new InputStreamReader(     connection.getInputStream(), encoding));// 往对端写完数据对端服务器返回数据   // ,以BufferedReader流来读取   StringBuffer buffer = new StringBuffer();   String line = "";   while ((line = reader.readLine()) != null) {    buffer.append(line);   }   reader.close();   return buffer.toString();  } catch (IOException e) {   e.printStackTrace();  } finally {   if (connection != null) {    connection.disconnect();// 关闭连接   }  }  return null; } /**  * unicode 转换成 中文  * @param theString  * @return  */ public static String decodeUnicode(String theString) {  char aChar;  int len = theString.length();  StringBuffer outBuffer = new StringBuffer(len);  for (int x = 0; x < len;) {   aChar = theString.charAt(x++);   if (aChar == '\\') {    aChar = theString.charAt(x++);    if (aChar == 'u') {     int value = 0;     for (int i = 0; i < 4; i++) {      aChar = theString.charAt(x++);      switch (aChar) {      case '0':      case '1':      case '2':      case '3':      case '4':      case '5':      case '6':      case '7':      case '8':      case '9':       value = (value << 4) + aChar - '0';       break;      case 'a':      case 'b':      case 'c':      case 'd':      case 'e':      case 'f':       value = (value << 4) + 10 + aChar - 'a';       break;      case 'A':      case 'B':      case 'C':      case 'D':      case 'E':      case 'F':       value = (value << 4) + 10 + aChar - 'A';       break;      default:       throw new IllegalArgumentException(         "Malformed      encoding.");      }     }     outBuffer.append((char) value);    } else {     if (aChar == 't') {      aChar = '\t';     } else if (aChar == 'r') {      aChar = '\r';     } else if (aChar == 'n') {      aChar = '\n';     } else if (aChar == 'f') {      aChar = '\f';     }     outBuffer.append(aChar);    }   } else {    outBuffer.append(aChar);   }  }  return outBuffer.toString(); } // 测试 public static void main(String[] args) {  IPUtils addressUtils = new IPUtils();  // 测试ip 219.136.134.157 中国=华南=广东省=广州市=越秀区=电信  String ip = "125.70.11.136";  String address = "";  try {   address = addressUtils.getAddresses("ip="+ip, "utf-8");  } catch (UnsupportedEncodingException e) {   // TODO Auto-generated catch block   e.printStackTrace();  }  System.out.println(address);  // 输出结果为:广东省,广州市,越秀区 }}  
====================================今天记录下类加载器classloader=======================================
类加载器任务:负责加载java类的字节码文件代码到java虚拟机中。
他使得java类可以被 动态的加载到java虚拟机中 并执行
一般开发人员不需要直接同类加载器进行交互。java虚拟机默认的行为已经足够大多数情况需要了。
不过遇到需要与类加载器今夕交互的情况,如果不清楚原理,则会遇到ClassNotFoundexception 和NoClassDefFondError异常

1.java源代码(.java文件)-----java编译器【动态生成】--------
2.java字节代码(.class文件)-----类加载器【classloader】----把.class文件转换成
3.java.lang.Class类的一个实例,每个实例用来表示一个java类。通过实例的newInstance()方法-------
4.创建该类的一个对象

java.lang.ClassLoader类

1.根据指定的类名称,找到或者生成对应的字节代码,从字节码中定义出一个java类,即java.lang.Class类的实例,
2.负责加载java应用所需的资源【如图像文件和配置文件】

ClassLoader提供的方法

表 1. ClassLoader 中与加载类相关的方法
方法说明getParent()返回该类加载器的父类加载器。loadClass(String name)加载名称为 name的类,返回的结果是 java.lang.Class类的实例。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 类。
对于 表 1中给出的方法,表示类名称的 name参数的值是类的二进制名称。需要注意的是内部类的表示,如 com.example.Sample$1com.example.Sample$Inner等表示方式。这些方法会在下面介绍类加载器的工作机制时,做进一步的说明。下面介绍类加载器的树状组织结构。

类加载器的树状组织结构


java中类加载器分成两类:
1.系统提供
  • a.引导类加载器bootstrap class loader:    用来加载java核心库,用源代码实现,不继承java.lang.ClassLoader
  • b.扩展类加载器【extensions class loader】:用来加载java扩展库,java虚拟机的实现会提供一个扩展目录。该加载器在此目录里查找并加载java类

  • c.系统类加载器【system class loader】根据java应用类类路径(classpath)来加载java类。一般来说java应用的类都是它来完成加载的
  • 通过Classloader.getSystemClassLoader()来获取它

2.开发人员自己开发
d.通过继承java.lang.ClassLoader类,来实现自己的类加载器,以满足一些特殊需求。

【除了a以外,bcd都有一个父类加载器,通过表1中getParent()方法得到】下面是他们的树状结构

类加载器树状组织结构示意图



public class ClassLoaderTree{
public staitc void main(String[] args){
ClassLoader loader = ClassLoaderTree.class.getClassLoader();//每个java类都维护着一个指向他的类加载器的引用,这样得到它
while(loader!=null){
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
输出如下:
 sun.misc.Launcher$AppClassLoader@9304b1  sun.misc.Launcher$ExtClassLoader@190d11

每一个java类都维护着一个【指向定义它的类加载器的】引用,通过getClassLoader()方法可以获取到此引用。
上面代码中通过递归调用getParent()方法来输出全部的父类加载器。

第一个输出的是ClassLoaderTree类的类加载器,即系统类加载器,是sun.misc.Launcher$AppClassLoader类的实例
第二个输出的是扩展类加载器,是sun.misc.Launcher$ExtClassLoader类的实例
并没有输出引导类加载器,这是由于有些JDK的实现对于 父类加载器  是引导类加载器的情况,getParent()方法返回null


类加载器的代理模式


类加载器在尝试自己去查找某个类的字节代码并定义时,先代理给其父类加载器,由父类加载器去尝试加载这个类,依次类推

在介绍代理模式背后的动机之前,先说明下java虚拟机如何判定两个java类相同

java虚拟机不仅要看
1.类的全名是否相同,
2.加载此类的类加载器是否一样
只有1和2都相同才认为两个类相同。即便是相同的字节代码,被不同的类加载器加载后所得到的类也是不同的。
举例:
java类:com.example.Sample  编译后生成Sample.class
两个不同的类加载器:ClassLoaderA和ClassLoaderB分别读取了这个Sample.class文件,并定义出两个java.lang.Class类的实例来表示这个类
这两个实例是不同的。对于jvm来说他们是不同的类

清单 3. com.example.Sample 类
 package com.example;  public class Sample {     private Sample instance;     public void setSample(Object instance) {         this.instance = (Sample) instance;     }  }

如 代码清单 3所示,com.example.Sample类的方法 setSample接受一个 java.lang.Object类型的参数,并且会把该参数强制转换成com.example.Sample类型。测试 Java 类是否相同的代码如 代码清单 4所示。

清单 4. 测试 Java 类是否相同


package classloader;import java.lang.reflect.Method;public class ClassIdentity {public static void main(String[] args) {new ClassIdentity().testClassIdentity();}public void testClassIdentity() {String classDataRootPath = "G:\\xiaoyang\\classloader\\bin";//这里的FileSystemClassLoader就是自己开发的类加载器FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);String className = "com.example.Sample";try {Class<?> class1 = fscl1.loadClass(className);Object obj1 = class1.newInstance();Class<?> class2 = fscl2.loadClass(className);Object obj2 = class2.newInstance();Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);setSampleMethod.invoke(obj1, obj2);} catch (Exception e) {e.printStackTrace();} }}








代码清单 4中使用了类 FileSystemClassLoader的两个不同实例来分别加载类 com.example.Sample,得到了两个不同的 java.lang.Class的实例,接着通过 newInstance()方法分别生成了两个类的对象 obj1和 obj2,最后通过 Java 的反射 API 在对象 obj1上调用方法 setSample,试图把对象 obj2赋值给 obj1内部的 instance对象。代码清单 4的运行结果如 代码清单 5所示。

清单 5. 测试 Java 类是否相同的运行结果
java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)at java.lang.reflect.Method.invoke(Method.java:597) at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26) at classloader.ClassIdentity.main(ClassIdentity.java:9) Caused by: java.lang.ClassCastException: com.example.Sample cannot be cast to com.example.Sample at com.example.Sample.setSample(Sample.java:7) ... 6 more

从 代码清单 5给出的运行结果可以看到,运行时抛出了 java.lang.ClassCastException异常。虽然两个对象 obj1和 obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。


===========ok准备工作结束============

了解了这点以后,就可以理解代理模式的设计动机了。

代理模式为保证java核心库的类型安全所有java应用都至少需要引用java.lang.Object类,也就是说

在运行的时候,java.lang.Object类需要被加载到java虚拟机中。如果这个加载过程由java应用自己的类加载器来完成的话,

很可能存在多个版本的java.lang.Object类,而且这些类是不兼容的。

通过代理模式,对java核心库的类加载工作由引导加载器来统一完成,保证java应用所使用的都是同一版本的java核心库类,

不同的类加载器为相同名称的类创建了额外的名称空间。

相同名称的类可以并存在java虚拟机中,只需要不同的类加载器来加载他们即可。

不同类加载器加载的类之间是不兼容的。这就相当于在java虚拟机内部创建了一个个相互隔离的java类空间

这种技术在许多框架中被用到


加载类的过程

类的加载分两个过程:
1.完成类的加载工作 :类定义加载器defining loader
2.启动这个加载过程 :   初始加载器initiating  loader

二者的关联是:一个类的1是它应用的其他类的2
如类com.example.Outer引用了类com.example.Inner
则由类com.example.Outer的定义加载器1负责启动inner的加载过程
【其实就是一个嵌套的过程】


jvm判断两个类是否相同时,使用的是类定义加载器
也就是说:
哪个类加载器启动类(initiating loader)的过程2:并不重要,
重要的是1:最终定义这个类的加载器(defining loader)

方法:loadClass() 抛出的是java.lang.classNotFoundException

方法:defineClass() 抛出的是java.lang.NoClassDefFoundeError

类加载器在成功加载某个类后,会把得到的java.lang.Class类的实例缓存起来,下次再请求加载该类的时候,

类加载器直接使用缓存的类的实例。而不会尝试再次加载。也就是

对于一个类加载实例来说,相同全名的类只加载一次,【loadClass方法不会被重复调用】

下面讨论另外一种类加载器:线程上下文类加载器

 

线程【Thread】上下文类加载器

线程上下文类加载器(context class loader)从JDK1.2开始有

类java.lang.Thread中的方法:获取和设置线程的上下文类加载器;

getContextClassLoader()

setContextClassLoader(ClassLoader cl)

如果没有通过setContextLoader设置的话,线程会 继承父线程的上下文类加载器。

java应用运行的初始【线程上下文类加载器】是【系统类加载器】,在线程中运行代码可以通过此类来加载类和资源


前面提到的类加载器的代理模式并不能解决java应用开发中遇到的类加载器的全部问题。

java提供了很多服务提供者接口SPI(service  provider interface)允许第三方为这些接口提供实现

常见的SPI有JDBC  JCE  JNDI  JAXP  JBI等,

这些SPI的接口java核心库提供实现。如JAXP的SPI接口定义包含在javax.xml.parsers包中

这些SPI的实现很可能作为java应用依赖的jar包被包含进来,通过类路径(classpath)来找到,

SPI接口的代码经常需要加载具体的实现类。


这里的实例的真正的类是继承【extends】自javax.xml.parsers.DocumentBuilderFactory,由SPI的实现所提供

JAXP中现实类是                          javax.Xml.Parsers.DocumentBuilderFactory类中的newInstance()方法用来生成一个新的DocumentBuilderFactory实例                                

ApacheXerces中实现类是org.apache.xerces.jaxp.DocumentBuilderFactoryImpl

而问题在于,
SPI的接口是ava核心库的一部分,是由引导类加载器来加载的,

spi的实现java类一般是由系统类加载器来加载的

引导类加载器 无法找到spi实现类,因为它只加载java核心库,但是线程上下文加载器正好解决了这个问题


java应用的【线程上下文类加载器】默认就是系统的上下文类加载器。

在spi接口的代码中使用线程上下文类加载器,可以成功的加载到spi实现的类

下面介绍另外一种加载类的方法:Class.forName

Class.forName

class.forName是静态方法,同样可以用来加载类。
该类的两种形式:
形式一:
/**
name表示的是类的全名
initialize表示是否初始化
loader表示加载时使用的类加载器
*/
Class.forName(String name ,boolean initialize,ClassLoader loader)
形式二:
/**
相当于设置了参数initialize值为true,
loader的值为当前类的类加载器
*/
Class.forName(String className);

Class.forName的一个很常见的用法是加载数据库驱动的时候
如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载Apache Derby数据库的驱动;

------------------开发自己的类加载器-------------------

虽然在绝大多数情况下,系统默认提供的类加载器实现客运满足需求:

但某些情况下,您还是需要为应用开发出自己的类加载器。

比如你的应用通过网络来传输java类的字节代码,为了保证安全,这些代码经过了加密处理,这个时候您需要自己的类加载器

1.从某个网络地址读取加密后的字节代码

2.接着精心解密和验证,

3.最后定义出要在java虚拟机中运行的类来。下面是两个实例


文件系统类加载器

自己开发的类加载器用来加载存储在文件系统上的 Java 字节代码。完整的实现如 代码清单 6所示。

清单 6. 文件系统类加载器
 public class FileSystemClassLoader extends ClassLoader {     private String rootDir;     public FileSystemClassLoader(String rootDir) {         this.rootDir = rootDir;     }     protected Class<?> findClass(String name) throws ClassNotFoundException {         byte[] classData = getClassData(name);         if (classData == null) {             throw new ClassNotFoundException();         }         else {             return defineClass(name, classData, 0, classData.length);         }     }     private byte[] getClassData(String className) {         String path = classNameToPath(className);         try {             InputStream ins = new FileInputStream(path);             ByteArrayOutputStream baos = new ByteArrayOutputStream();             int bufferSize = 4096;             byte[] buffer = new byte[bufferSize];             int bytesNumRead = 0;             while ((bytesNumRead = ins.read(buffer)) != -1) {                 baos.write(buffer, 0, bytesNumRead);             }             return baos.toByteArray();         } catch (IOException e) {             e.printStackTrace();         }         return null;     }     private String classNameToPath(String className) {         return rootDir + File.separatorChar                 + className.replace('.', File.separatorChar) + ".class";     }  }

如 代码清单 6所示,类 FileSystemClassLoader继承自类 java.lang.ClassLoader。在 表 1中列出的 java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可

java.lang.ClassLoader类的方法 loadClass()封装了前面提到的代理模式的实现

loadClass()的调用步骤:

1.该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;

2.如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类

3.如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。

因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,

最好不要覆写 loadClass()方法,而是覆写 findClass()方法

类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例



网络类加载器

下面将通过一个网络类加载器】来说明如何通过类加载器来实现组件的动态更新

即基本的场景是:

Java 字节代码(.class)文件存放在服务器上,

客户端通过网络的方式获取字节代码并执行当有版本更新的时候,只需要替换掉服务器上保存的文件即可

通过类加载器可以比较简单的实现这种需求。


客户端用自己开发的类加载器【NetworkClassLoader】:

类 NetworkClassLoader负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与 FileSystemClassLoader类似。

在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它

第一种做法是使用 Java 反射 API。

第二种做法是使用接口。

需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。

使用 Java 反射 API 可以直接调用 Java 类的方法

而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。网络类加载器的具体代码见 下载


在介绍完如何开发自己的类加载器之后,下面说明类加载器和 Web 容器的关系。

类加载器与 Web 容器

对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:

  • 每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
  • 多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
  • 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

在介绍完类加载器与 Web 容器的关系之后,下面介绍它与 OSGi 的关系。

类加载器与 OSGi

OSGi™是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse 就是基于 OSGi 技术来构建的。

OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。

假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类,都是由父类加载器来加载的。如果声明了系统属性 org.osgi.framework.bootdelegation=com.example.core.*,那么对于包com.example.core中的类,都是由父类加载器来完成的。

OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:

  • 如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。
  • 如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
  • 如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。

总结

类加载器是 Java 语言的一个创新。它使得动态安装和更新软件组件成为可能。本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器、与 Web 容器和 OSGi 的关系等。开发人员在遇到 ClassNotFoundException和 NoClassDefFoundError等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。

====================有关java的移位操作========================


java中有三种移位运算符[<<左移的位数]

<<      :     左移运算符,num << 1,相当于num乘以2

>>      :     右移运算符,num >> 1,相当于num除以2

>>>    :     无符号右移,忽略符号位,空位都以0补齐

下面来看看这些移位运算都是怎样使用的

public class Test {    public static void main(String[] args) {        int number = 10;        printInfo(number);//原始数二进制</span>        number = number << 1;//左移一位</span>
         printInfo(number);         number = number >> 1;//右移一位         printInfo(number);    }        /**     * 输出一个int的二进制数     * @param num     */    private static void printInfo(int num){        System.out.println(Integer.toBinaryString(num));    }}</span>

运行结果为:

1010101001010
结论:
左移一位  原数据乘以2
左移两位  是原数据乘以4
左移n位,是乘以2的n次幂

右移一位 是除以2
对于:>>>
只记住一点:忽略了符号位扩展,0补最高位  无符号右移运算符>>> 只是对32位和64位的值有意义

=====================深入解析HashMap、HashTable===========================

如面试官问你:HashMap和HashTable有什么区别,一个比较简单的回答是:

1、HashMap是非线程安全的,HashTable是线程安全的。

2、HashMap的键和值都允许有null值存在,而HashTable则不行。

3、因为线程安全的问题,HashMap效率比HashTable的要高。

Java中的另一个线程安全的与HashMap极其类似的类是什么
同样是线程安全,它与HashTable在线程同步上有什么不同
从最近的一些面试说起吧,感受就是:知识是永无止境的,永远不要觉得自己已经掌握了某些东西。如果对哪一块知识感兴趣,那么,请多多的花时间,哪怕最基础的东西也要理解它的原理,尽量往深了研究,在学习的同时,记得多与大家交流沟通,因为也许某些东西,从你自己的角度,是很难发现的,因为你并没有那么多的实验环境去发现他们。只有交流的多了,才能及时找出自己的不足,才能认识到:“哦,原来我还有这么多不知道的东西!”。
HashMap内部存储结构:

Java中数据存储方式最底层的两种结构,

一种是数组特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢

一种是链表,特点:空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”,如下图:

从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,【这里为什么用16,因为默认的大小是capacity大小是16】

每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,

定位置

12%16=12,

28%16=12,

108%16=12,

140%16=12。

所以12、28、108以及140都存储在数组下标为12的位置。

它的内部其实是用一个Entity数组来实现的,属性有key、value、next。接下来我会从初始化阶段详细的讲解HashMap的内部结构。

1、初始化
首先来看三个常量:
static final int DEFAULT_INITIAL_CAPACITY = 16;    初始容量:16
static final int MAXIMUM_CAPACITY = 1 << 30;         最大容量:2的30次方:1073741824    左移30位也就是2的30次幂
static final float DEFAULT_LOAD_FACTOR = 0.75f; 装载因子,扩充数组大小的时候用
来看个无参构造方法,也是我们最常用的:

[java] view plaincopy
  1. public HashMap() {  
  2.         this.loadFactor = DEFAULT_LOAD_FACTOR;  
  3.         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
  4.         table = new Entry[DEFAULT_INITIAL_CAPACITY];  
  5.         init();  
  6.     }  

loadFactor、threshold的值在此处没有起到作用,不过他们在后面的扩容方面会用到,此处只需理解table=new Entry[DEFAULT_INITIAL_CAPACITY].说明,默认就是开辟16个大小的空间。另外一个重要的构造方法:

[java] view plaincopy
  1. public HashMap(int initialCapacity, float loadFactor) {  
  2.         if (initialCapacity < 0)  
  3.             throw new IllegalArgumentException("Illegal initial capacity: " +  
  4.                                                initialCapacity);  
  5.         if (initialCapacity > MAXIMUM_CAPACITY)  
  6.             initialCapacity = MAXIMUM_CAPACITY;  
  7.         if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  8.             throw new IllegalArgumentException("Illegal load factor: " +  
  9.                                                loadFactor);  
  10.   
  11.         // Find a power of 2 >= initialCapacity  
  12.         int capacity = 1;  
  13.         while (capacity < initialCapacity)  
  14.             {capacity <<= 1; } 
  15.   
  16.         this.loadFactor = loadFactor;  
  17.         threshold = (int)(capacity * loadFactor);  
  18.         table new Entry[capacity];  
  19.         init();  
  20.     }  


就是说传入参数的构造方法,我们把重点放在:【这里很重要,因为是左移2的n次幂,所以一定是偶数。。】

[java] view plaincopy
  1. while (capacity < initialCapacity)  
  2.            capacity <<= 1;  

因为是循环,所以多次循环后的值为比参数大的2的n次幂的数值
上面,该代码的意思是,实际的开辟的空间要大于传入的第一个参数的值。举个例子:
new HashMap(7,0.8),

loadFactor为0.8,capacity为7,通过上述代码后,capacity的值为:8.(1 << 2的结果是4,2 << 2的结果为8)。

所以,最终capacity的值为8,最后通过new Entry[capacity]来创建大小为capacity的数组,所以,这种方法最红取决于capacity的大小。



2、put(Object key,Object value)操作
 
当调用put操作时,首先判断key是否为null,如下代码1处:

[java] view plaincopy
  1. <p>public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());  
  5.         int i = indexFor(hash, table.length);  //这里就是上图hashMap结构中数据的大小
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }</p><p>        modCount++;  
  15.         addEntry(hash, key, value, i);  
  16.         return null;  
  17.     }</p>  


如果key是null,则调用如下代码:

[java] view plaincopy
  1. private V putForNullKey(V value) {  
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
  3.             if (e.key == null) {  
  4.                 V oldValue = e.value;  
  5.                 e.value = value;  
  6.                 e.recordAccess(this);  
  7.                 return oldValue;  
  8.             }  
  9.         }  
  10.         modCount++;  
  11.         addEntry(0null, value, 0);  
  12.         return null;  
  13.     }  


就是说,获取Entry的第一个元素table[0],并基于第一个元素的next属性开始遍历,直到找到key为null的Entry,将其value设置为新的value值。
如果没有找到key为null的元素,则调用如上述代码的addEntry(0, null, value, 0);增加一个新的entry,代码如下:

[java] view plaincopy
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     Entry<K,V> e = table[bucketIndex];  
  3.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.         if (size++ >= threshold)  
  5.             resize(2 * table.length);  
  6.     }  


先获取第一个元素table[bucketIndex],传给e对象,新建一个entry,key为null,value为传入的value值,next为获取的e对象。如果容量大于threshold,容量扩大2倍。
如果key不为null,这也是大多数的情况,重新看一下源码:

[java] view plaincopy
  1. public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());//---------------2---------------  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {//--------------3-----------  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }//-------------------4------------------  
  15.         modCount++;//----------------5----------  
  16.         addEntry(hash, key, value, i);-------------6-----------  
  17.         return null;  
  18.     }  


看源码中2处,首先会进行key.hashCode()操作,获取key的哈希值,hashCode()是Object类的一个方法,为本地方法,内部实现比较复杂,我们
会在后面作单独的关于Java中Native方法的分析中介绍。hash()的源码如下:

[java] view plaincopy
  1. static int hash(int h) {  
  2.         // This function ensures that hashCodes that differ only by  
  3.         // constant multiples at each bit position have a bounded  
  4.         // number of collisions (approximately 8 at default load factor).  
  5.         h ^= (h >>> 20) ^ (h >>> 12);  
  6.         return h ^ (h >>> 7) ^ (h >>> 4);  
  7.     }  

int i = indexFor(hash, table.length);的意思,相当于int i = hash % Entry[].length;得到i后,就是在Entry数组中的位置,(上述代码5和6处是如果Entry数组中不存在新要增加的元素,则执行5,6处的代码,如果存在,即Hash冲突,则执行 3-4处的代码,此处HashMap中采用链地址法解决Hash冲突。此处经网友bbycszh指正,发现上述陈述有些问题)。重新解释:其实不管Entry数组中i位置有无元素,都会去执行5-6处的代码,如果没有,则直接新增,如果有,则将新元素设置为Entry[0],其next指针指向原有对象,即原有对象为Entry[1]。具体方法可以解释为下面的这段文字:(3-4处的代码只是检查在索引为i的这条链上有没有key重复的,有则替换且返回原值,程序不再去执行5-6处的代码,无则无处理

上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。如, 第一个键值对A进来,通过计算其key的hash得到的i=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其i也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,i也等于0,那么C.next = B,Entry[0] = C;这样我们发现i=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起,也就是说数组中存储的是最后插入的元素。

到这里为止,HashMap的大致实现,我们应该已经清楚了。当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个i的链就会很长,会不会影响性能?HashMap里面设置一个因素(也称为因子),随着map的size越来越大,Entry[]会以一定的规则加长长度。

=======================================ThreadLocal总结===============================

今天比较忙,先引出个标题,等今天问题解决再考虑这个

参考文档:http://www.iteye.com/topic/103804

自己的代码:

import com.jt.core4.util.StringUtil;public class AuthenticatorHelper {public static ThreadLocal<String> treadSession=new ThreadLocal<String>();public static ThreadLocal<Integer> treadUser=new ThreadLocal<Integer>();public static String getSessionId()  {  String sessionId=(String) treadSession.get();           if (sessionId == null||sessionId.length()==0) {          sessionId=StringUtil.getRandomString(32);        treadSession.set(sessionId);          }      return sessionId;  }  public static void setSessionId(String sessionId){treadSession.set(sessionId);}public static int getUserId()  {  Integer userId=(Integer) treadUser.get();  if(userId==null)return 0;elsereturn userId;  }public static void setUserId(int userId){treadUser.set(userId);}}


===============================异常总结,============================
今天遇到很恶心的问题:如下:


==========================自己定义jsp页面标签开发======================

步骤就三个:
1.自己开发标签处理类【核心继承TagSupport类,重新doStart().doEnd()方法】
super.pageContext.getOut().write(targetTime);
这句最核心,就是往页面写入文本调用
2.配置标签的tld文件xxx.tld【放在WEB-INF目录下的一个文件夹中】
3.修改web.xml文件,添加taglib项,指定到2中的文件路径
4.jsp页面引入适用

首先,得导入jar包  jsp-api-2.2-sources.jar

(如果你的项目中使用了maven可以在pom.xml文件中添加

Xml代码  收藏代码
  1. <dependency>  
  2. <groupId>javax.servlet.jsp</groupId>  
  3. <artifactId>jsp-api</artifactId>  
  4. <version>2.2</version>  
  5. <scope>provided</scope>  
  6. </dependency>  

 jar文件引用。

)

第二步,定义一个用来实现标签功能的java类,例如:DateConvert.java

Java代码  收藏代码
  1. import java.io.IOException;  
  2. import java.text.SimpleDateFormat;  
  3. import java.util.Date;  
  4. import javax.servlet.jsp.JspException;  
  5. import javax.servlet.jsp.tagext.TagSupport;  
  6.   
  7. /** 
  8.  * 数据类型转换 
  9.  * @author LiDuanqiang 
  10.  * 
  11.  */  
  12. @SuppressWarnings("serial")  
  13. public class DateConvert extends TagSupport{  
  14.     private String longTime;  
  15.       
  16.     @Override  
  17.     public int doStartTag() throws JspException {  
  18.         long l = 0l;  
  19.         if (longTime!=null&&!longTime.equals("")) {  
  20.             l = Long.parseLong(longTime);  
  21.         }  
  22.         Date date = new Date(l);  
  23.         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  24.         String targetTime = format.format(date);  
  25.         try {  
  26.             super.pageContext.getOut().write(targetTime);  
  27.         } catch (IOException e) {  
  28.             e.printStackTrace();  
  29.         }  
  30.         return super.doStartTag();  
  31.     }  
  32.       
  33.     //setter and getter  
  34.     public void setLongTime(String longTime) {  
  35.         this.longTime = longTime;  
  36.     }  
  37.       
  38. }  

  第三步,可在WEB-INF目录下定义一个*.tld文件,例如dateConvert.tld:

Xml代码  收藏代码
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"  
  3.                         "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">  
  4. <taglib>  
  5.  <tlib-version>1.0</tlib-version>  
  6.  <jsp-version>1.2</jsp-version>  
  7.  <short-name>ct</short-name>  
  8.  <uri>/dateConvert</uri>  
  9.    
  10.  <tag>  
  11.     <name>longStr</name>  
  12.     <tag-class>org.up.snapshot.utils.DateConvert</tag-class>  
  13.     <body-content>JSP</body-content>  
  14.     <attribute>  
  15.         <name>longTime</name>  
  16.         <required>true</required>  
  17.         <rtexprvalue>true</rtexprvalue>  
  18.     </attribute>  
  19.  </tag>  
  20. </taglib>  

 第四步,在web.xml文件中引用你的*.tld文件:

Xml代码  收藏代码
  1. <taglib>  
  2.         <taglib-uri>/dateConvert</taglib-uri>  
  3.         <taglib-location>dateConvert.tld</taglib-location>  
  4.     </taglib>  
  5. <welcome-file-list>  
  6.         <welcome-file>dateConvert.jsp</welcome-file>  
  7.     </welcome-file-list>  

 第五步,在你的页面引入自定义标签库进行使用,例如:dateConvert.jsp:

Jsp代码  收藏代码
  1. <%@ page language="java" contentType="text/html; charset=utf-8"    pageEncoding="utf-8"%>  
  2. <%@ taglib uri="/dateConvert" prefix="ct"%>  
  3. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">  
  4. <html>  
  5. <head>  
  6. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">  
  7. <title>数据类型转换</title>  
  8. </head>  
  9. <body>  
  10. <ct:longStr longTime="1314842011312"></ct:longStr>  
  11. </body>  
  12. </html>  
 

 以上代码实现的是将长整型的数据通过自定义标签转换成指定日期格式进行输出。当然,大家可以定义功能更加强大的java类来实现你的标签功能。

 



0 0
原创粉丝点击