一、语法
1.volatile,
这个果然是个大问题,到处搜搜再理解理解吧,jdk早期版本就不考虑了,针对jdk1.6
理解下来有两点:
- 线程加载volatile变量时始终从主存加载
- 保证编译顺序,引用“在JDK1.5及其后续版本中,扩充了volatile语义,系统将不允许对 写入一个volatile变量的操作与其之前的任何读写操作 重新排序,也不允许将 读取一个volatile变量的操作与其之后的任何读写操作 重新排序”
查这个时看到通常的双重检查单例模式在java中无效,很是震惊,太深了,有兴趣的再研究。
另外注意的是如果修饰对象或数组,则作用的是引用的对象的地址,不是作用的变量的值。
2.final,变量一经初始化就不能指向其它对象。指向的存储地址不可修改,但指向的对象本身是可以修改的。
3.保留小数
double f = 111231.5585; public static void m1() { BigDecimal bg = new BigDecimal(f); double f1 = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); System.out.println(f1); //111231.56 } public static void m2() { DecimalFormat df = new DecimalFormat("#.00");//强制2位(绝对值>=1,"#.00";否则,"0.00") System.out.println(df.format(f)); //111231.56 } public static void m3() { System.out.println(String.format("%.2f", f));//强制2位 111231.56 } public static void m4() { NumberFormat nf = NumberFormat.getNumberInstance(); nf.setMaximumFractionDigits(2); System.out.println(nf.format(f)); //111,231.56 }
4.内部类
- 成员内部类,作为外部类的成员,可以直接使用外部类的所有成员和方法,即使是private的。同时外部类要访问内部类的所有成员变量/方法,则需要通过内部类的对象来获取。
1 public class Outer { 2 public static void main(String[] args) { 3 Outer outer = new Outer(); 4 Outer.Inner inner = outer.new Inner(); 5 inner.print("Outer.new"); 6 7 inner = outer.getInner(); 8 inner.print("Outer.get"); 9 } 10 11 public Inner getInner() { 12 return new Inner(); 13 } 14 15 public class Inner { 16 public void print(String str) { 17 System.out.println(str); 18 } 19 } 20 }
- 局部内部类,是指内部类定义在方法和作用域内。
1 public class Parcel4 { 2 public Destination destination(String s) { 3 class PDestination implements Destination { 4 private String label; 5 6 private PDestination(String whereTo) { 7 label = whereTo; 8 } 9 10 public String readLabel() { 11 return label; 12 } 13 } 14 return new PDestination(s); 15 } 16 17 public static void main(String[] args) { 18 Parcel4 p = new Parcel4(); 19 Destination d = p.destination("Tasmania"); 20 } 21 }
定义在作用域里:
1 public class Parcel5 { 2 private void internalTracking(boolean b) { 3 if (b) { 4 class TrackingSlip { 5 private String id; 6 TrackingSlip(String s) { 7 id = s; 8 } 9 String getSlip() { 10 return id; 11 } 12 } 13 TrackingSlip ts = new TrackingSlip("slip"); 14 String s = ts.getSlip(); 15 } 16 } 17 18 public void track() { 19 internalTracking(true); 20 } 21 22 public static void main(String[] args) { 23 Parcel5 p = new Parcel5(); 24 p.track(); 25 } 26 }
- 嵌套内部类,就是修饰为static的内部类。声明为static的内部类,不需要内部类对象和外部类对象之间的联系,就是说我们可以直接引用outer.inner,即不需要创建外部类,也不需要创建内部类。
- 匿名内部类,有时候我为了免去给内部类命名,便倾向于使用匿名内部类,因为它没有名字。
5.泛型
泛型类的定义:public class Test<T>{}
泛型方法:
6.集合
6.1 List
- ArrayList,采用数组方式存储数据,索引数据快插入数据慢
- Vector,采用数组方式存储数据,索引数据快插入数据慢,使用了synchronized方法,性能上比ArrayList要差
- LinkedList,使用双向链表实现存储,索引数据慢,插入数据快
6.2 Set
不允许包含相同的元素,根据hashCode和equals判断相等
- HashSet,基于HashMap,value是个空Object对象
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { private transient HashMap<E,Object> map; ... }
有以下特点
(1).不能保证元素的排列顺序,顺序有可能发生变化
(2).不是同步的
(3).集合元素可以是null,但只能放入一个null
- LinkedHashSet,继承自HashSet,使用LinkedHashMap来保存所有元素,维护元素的次序。这样使得元素看起 来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet
- TreeSet,基于TreeMap
6.3 Map
- HashMap,最多只允许一条记录的键为Null,允许多条记录的值为 Null,存入的键值对在取出的时候是不确定的
- LinkedHashMap,继承自HashMap,取出顺序和插入顺序是一样的
- TreeMap,取出来的是排序后的键值对,根据comparator比较器或key的comparable接口
- Hashtable,不允许记录的键或者值为null
ps:jdk1.8下,HashMap的数据结构有了很大变化:数组+链表+红黑树,当链表长度到达8时会对数组进行调整,如果数组长度<64,那么进行扩容,否则将链表调整为红黑树
7.枚举
- 基本使用:
public enum ColorEnum { red, green, yellow, blue; }
- 使用自定义字符串表示并保留序号:
public enum ColorEnum { red {public String getName(){return "红色";}}, green{public String getName(){return "绿色";}}, yellow{public String getName(){return "黄色";}}, blue{public String getName(){return "蓝色";}}; public abstract String getName(); }
- 使用自定义字符串,不保留序号:
public enum Gender{ MAN("MAN"), WOMEN("WOMEN"); private final String value; //构造器默认也只能是private, 从而保证构造函数只能在内部使用 Gender(String value) { this.value = value; } public String getValue() { return value; } // 覆盖方法 public String toString() { return String.valueOf (this.value); } }
- 使用自定义字符串和序号(或字符串):
public enum MsgStatus{ UNSEND(0){ public String getText(){ return "未发送"; } }, NEW(1){ public String getText(){ return "已发送新增报"; } }; private int v; private MsgStatus(int _v) { this.v = _v; } public abstract String getText(); public int getValue(){ return this.v; } }
8.访问修饰符类型(public,protected,private,friendly)
- public的类、类属变量及方法,包内及包外的任何类均可以访问;
- protected的类、类属变量及方法,包内的任何类,及包外的那些继承了此类的子类才能访问。
- private的类、类属变量及方法,包内包外的任何类均不能访问;
- 如果一个类、类属变量及方法不以这三种修饰符来修饰,它就是friendly类型的,那么包内的任何类都可以访问它,而包外的任何类都不能访问它(包括包外继承了此类的子类),因此,这种类、类属变量及方法对包内的其他类是友好的,开放的,而对包外的其他类是关闭的
9.获取系统属性
1 System.getProperty("os.name"); //操作系统名称 2 Charset.defaultCharset().name(); //jvm默认字符集 3 System.getProperty("line.separator"); //行分隔符(windows是" ",linux是" ") 4 System.getProperty("file.separator"); //文件目录分隔符(windows是"", linux是"/") 5 System.getProperty("path.separator"); //路径分隔符(windows";",linux是":") 6 System.getProperty("file.encoding"); //影响文件内容 7 System.setProperty("sun.jnu.encoding"); //影响文件名的创建
10.日期格式化
通常用SimpleDateFormat,我就经常使用new SimpleDateFormat("MM/dd/yy hh:mm a"),没想到有次竟然栽了个大跟头,SimpleDateFormat还有个参数Locale,没太在意,有次使用ftp4j 的FTPClient.list("/pre")才发现这个问题,估计ftp服务器返回"03/05/13 12:00 AM"这样的格式,而ftp4j使用new SimpleDateFormat("MM/dd/yy hh:mm a")来解析,结果出现FTPListParseException异常,修改本机地区语言中文改为:
就好了,或者改为使用new SimpleDateFormat("MM/dd/yy hh:mm a",Locale.ENGLISH)也可以,通过实验如果是"03/05/13 12:00 AM",要使用Locale.ENGLISH,如果是"03/05/13 12:00 上午",要使用Locale.CHINESE
11. Integer和int比较
Integer int1=new Integer(100); Integer int2=new Integer(100); System.out.println(int1==int2);//false,new出的Integer比较内存地址 Integer int1=Integer.valueOf(100); Integer int2=Integer.valueOf(100);//等同Integer int2=Integer.valueOf(100); System.out.println(int1==int2);//true,默认下valueOf的[-128,127]被缓存,因此内存地址相同 Integer int1=new Integer(100); Integer int2=100; System.out.println(int1==int2);//false,和第一种情况类似 Integer int1=new Integer(100); int int2=100; System.out.println(int1==int2);//true,拆包为int比较值
12.String和null
String str=null; System.out.println(null instanceof Object);//false System.out.println(str instanceof String);//false System.out.println((String)null instanceof String);//false
13.instanceOf,判断一个对象是否是某个特定类或者是它的子类的一个实例,当左操作符为null的时候返回false. 右操作符不能是null
14.String比较
String str1="123"; String str2="123"; System.out.println(str1==str2);//true,使用常量池,相同内存地址 String str1=new String("123"); String str2=new String("123"); System.out.println(str1==str2);//false,不同内存地址 String str1=new String("123"); String str2="123"; System.out.println(str1==str2);//false,不同内存地址
String str1="123";
String str2="12"+"3";
System.out.println(str1==str2);//true,使用常量池,相同的地址
再来理解:
- String.intern():
补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用
- str=”kv”+”ill”+” “+”ans”;
就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” “ 生成 ”kvill “存在内存中,最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的“不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的
网上摘得内容(没验证):
String str1="abc";
String str2="abc";
String str3="abc";
String str4=new String("abc");
String str5=new String("abc");
对于浅蓝色箭头,通过new操作产生一个字符串(“abc”)时,会先去常量池中查找是否有“abc”对象,如果没有则在常量池中创建一个此字符串对象,然后堆中再创建一个常量池中此“abc”对象的拷贝对象,所以,对于String str=new String("abc"),如果常量池中原来没有"abc"则产生两个对象,否则产生一个对象。
而对于基础类型的变量和常量,变量和引用存储在栈中,常量存储在常量池中。例如:
int a1=1,a2-1,a3=1; public static final int INT1=1; public static final int INT2=1; public static final int INT3=1;
15.HashMap,底层就是一个数组结构,数组中的每一项又是一个链表
当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部
16.String.format(),有两种重载形式:
format(String format, Object... args) 新字符串使用本地语言环境,指定字符串格式和参数生成格式化的新字符串。
format(Locale locale, String format, Object... args) 使用指定的语言环境,指定字符串格式和参数生成格式化的字符串
转 换 符 |
说 明 |
示 例 |
%s |
字符串类型 |
"mingrisoft" |
%c |
字符类型 |
'm' |
%b |
布尔类型 |
true |
%d |
整数类型(十进制) |
99 |
%x |
整数类型(十六进制) |
FF |
%o |
整数类型(八进制) |
77 |
%f |
浮点类型 |
99.99 |
%a |
十六进制浮点类型 |
FF.35AE |
%e |
指数类型 |
9.38e+5 |
%g |
通用浮点类型(f和e类型中较短的) |
|
%h |
散列码 |
|
%% |
百分比类型 |
% |
%n |
换行符 |
|
%tx |
日期与时间类型(x代表不同的日期与时间转换符 |
|
给几个 %tx 的例子
Date date=new Date(); // 创建日期对象 System.out.printf("全部日期和时间信息:%tc%n", date);//格式化输出日期或时间 System.out.printf("年-月-日格式:%tF%n", date); System.out.printf("月/日/年格式:%tD%n", date); System.out.printf("HH:MM:SS PM格式(12时制):%tr%n", date); System.out.printf("HH:MM:SS格式(24时制):%tT%n", date); System.out.printf("HH:MM格式(24时制):%tR%n", date);
输出
全部日期和时间信息:星期三 五月 04 16:50:46 CST 2016 年-月-日格式:2016-05-04 月/日/年格式:05/04/16 HH:MM:SS PM格式(12时制):04:50:46 下午 HH:MM:SS格式(24时制):16:50:46 HH:MM格式(24时制):16:50
/*b或者h: 月份简称,如 中:十月 英:Oct B: 月份全称,如 中:十月 英:October a: 星期的简称,如 中:星期六 英:Sat A: 星期的全称,如: 中:星期六 英:Saturday C: 年的前两位数字(不足两位前面补0),如:20 y: 年的后两位数字(不足两位前面补0),如:07 Y: 4位数字的年份(不足4位前面补0),如:2007 j: 一年中的天数(即年的第几天),如:300 m: 两位数字的月份(不足两位前面补0),如:10 d: 两位数字的日(不足两位前面补0),如:27 e: 月份的日(前面不补0),如:5 */ String str=String.format(Locale.US,"英文月份简称:%tb",date); System.out.println(str); System.out.printf("本地月份简称:%tb%n",date); str=String.format(Locale.US,"英文月份全称:%tB",date); System.out.println(str); System.out.printf("本地月份全称:%tB%n",date); str=String.format(Locale.US,"英文星期的简称:%ta",date); System.out.println(str); System.out.printf("本地星期的简称:%tA%n",date); System.out.printf("年的前两位数字(不足两位前面补0):%tC%n",date); System.out.printf("年的后两位数字(不足两位前面补0):%ty%n",date); System.out.printf("一年中的天数(即年的第几天):%tj%n",date); System.out.printf("两位数字的月份(不足两位前面补0):%tm%n",date); System.out.printf("两位数字的日(不足两位前面补0):%td%n",date); System.out.printf("月份的日(前面不补0):%te",date);
输出:
英文月份简称:May 本地月份简称:五月 英文月份全称:May 本地月份全称:五月 英文星期的简称:Wed 本地星期的简称:星期三 年的前两位数字(不足两位前面补0):20 年的后两位数字(不足两位前面补0):16 一年中的天数(即年的第几天):125 两位数字的月份(不足两位前面补0):05 两位数字的日(不足两位前面补0):04 月份的日(前面不补0):4
/*H: 2位数字24时制的小时(不足2位前面补0),如:15 I: 2位数字12时制的小时(不足2位前面补0),如:03 k: 2位数字24时制的小时(前面不补0),如:15 l: 2位数字12时制的小时(前面不补0),如:3 M: 2位数字的分钟(不足2位前面补0),如:03 S: 2位数字的秒(不足2位前面补0),如:09 L: 3位数字的毫秒(不足3位前面补0),如:015 N: 9位数字的毫秒数(不足9位前面补0),如:562000000 p: 小写字母的上午或下午标记,如: 中:下午 英:pm z: 相对于GMT的RFC822时区的偏移量,如:+0800 Z: 时区缩写字符串,如:CST s: 1970-1-1 00:00:00 到现在所经过的秒数,如:1193468128 Q: 1970-1-1 00:00:00 到现在所经过的毫秒数,如:1193468128984 */ System.out.printf("2位数字24时制的小时(不足2位前面补0):%tH%n",date); System.out.printf("2位数字12时制的小时(不足2位前面补0):%tI%n",date); System.out.printf("2位数字24时制的小时(前面不补0):%tk%n",date); System.out.printf("2位数字12时制的小时(前面不补0):%tl%n",date); System.out.printf("2位数字的分钟(不足2位前面补0):%tM%n",date); System.out.printf("2位数字的秒(不足2位前面补0):%tS%n",date); System.out.printf("3位数字的毫秒(不足3位前面补0):%tL%n",date); System.out.printf("9位数字的毫秒数(不足9位前面补0):%tN%n",date); String str1=String.format(Locale.US,"小写字母的上午或下午标记(英):%tp",date); System.out.println(str); // 输出字符串变量str的内容 System.out.printf ("小写字母的上午或下午标记(中):%tp%n",date); System.out.printf("相对于GMT的RFC822时区的偏移量:%tz%n",date); System.out.printf("时区缩写字符串:%tZ%n",date); System.out.printf("1970-1-1 00:00:00 到现在所经过的秒数:%ts%n",date); System.out.printf("1970-1-1 00:00:00 到现在所经过的毫秒数:%tQ%n",date);
输出:
2位数字24时制的小时(不足2位前面补0):16 2位数字12时制的小时(不足2位前面补0):04 2位数字24时制的小时(前面不补0):16 2位数字12时制的小时(前面不补0):4 2位数字的分钟(不足2位前面补0):50 2位数字的秒(不足2位前面补0):46 3位数字的毫秒(不足3位前面补0):337 9位数字的毫秒数(不足9位前面补0):337000000 英文星期的简称:Wed 小写字母的上午或下午标记(中):下午 相对于GMT的RFC822时区的偏移量:+0800 时区缩写字符串:CST 1970-1-1 00:00:00 到现在所经过的秒数:1462351846 1970-1-1 00:00:00 到现在所经过的毫秒数:1462351846337
二、命令
1.java和javaw命令
2.用idea跑项目时出现java.lang.OutOfMemoryError: PermGen space错误,就顺便查了下java命令运行时的一些参数
这个错误可以设置-XX:MaxPermSize=128m,增大持久代内存的值
还有个参数-XX:PermSize=64m,这个是设置初始持久代内存,网上说是物理内存的1/64,我本机看了下,并不是。我本机8G内存,用jinfo -flag PermSize查出才20多兆,
3.jvm监控
- jps,列出java进程,常用jps -l
- jinfo,观察进程运行环境参数,包括Java System属性和JVM命令行参数,常用jinfo -flag PermSize 9224,jinfo -flag MaxPermSize 9224
- 超级命令啊,jconsole/jvisualvm,这两个很强大,惊叹了
三、内存模型
java虚拟机运行的时候内存分配图如下图:
- jvm虚拟机栈:线程独有的,每次启动一个线程,就创建一个jvm虚拟机栈,线程退出的时候就销毁。这里面主要保存线程本地变量名和局部变量值。
- 本地方法栈: 调用本地jni方法的时候而创建的。这里分配的jvm之外的内存空间。方法调用结束之后销毁。
- 堆:主要保存创建的对象
- 方法区:保存class相关的信息。主要是class的一个内存结构信息,类型信息包括:
1)这个类型的完整有效名
2)这个类型直接父类的完整有效名(除非这个类型是interface或是java.lang.Object,两种情况下都没有父类)
3)这个类型的修饰符(public,abstract, final的某个子集)
4)这个类型直接接口的一个有序列表
除此外,还要为每个类型保存以下信息:
1)类型的常量池( constant pool)
2)域(Field)信息
3)方法(Method)信息
4)除了常量外的所有静态(static)变量
- 常量池:方法区的一部分,主要保存class内存结构中常量值 例如String值,public static final 类型的值
- 线程pc寄存器 : 这个保存线程当前执行的字节码指令,
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域
四、编码
涉及到编码一直让人头痛,java的编码要分清楚三个概念:Java采用的编码:unicode,JVM平台默认字符集和外部资源的编码。String 采用UTF-16编码方式存储所有字符,就是unicode编码,JVM平台默认字符集跟操作系统有关
另外要明确编码解码的概念:由字节到字符是解码,反之是编码
网上有许多类似new String(input.getBytes("ISO-8859-1"), "GBK") 这样的转换编码方式的代码,害人很深,我也深陷彀中。
在这种不正常的情况下,我们获取了正确的结果,但是当这种方式在特定情况下才是对的
应当记住只有在字符与字节转换时才需要指定编码,比如读取磁盘内容,肯定是字节,要想用String标识需要指定正确的编码转为unicode编码的String,字符串之间的转化没有意义
在网上看了一张图画了java中编码的各种使用
持续更新中...
参考文章:
2. JVM系列
4. JVM中方法区