有关java语法的一些细节(与c++比较)
本文记录一些javase的细节,多与c++进行比较。
java8种基本类型
数据类型名称 | 占用字节 | 默认值 | 包装类 |
byte(字节型) | 1 | 0 | Byte |
short(短整型) | 2 | 0 | Short |
char(字符型) | 2 | \u0000 | Character |
int(整形) | 4 | 0 | Integer |
long(长整形) | 8 | 0L | Long |
float(单精度浮点型) | 4 | 0.0f | Float |
double(双精度浮点数) | 8 | 0.0d | Double |
boolean(布尔值) | 1bit(JVM) | false | Boolean |
与c++不同,java不支持方法参数的默认值设置,因此若有此需要,可以使用重载的方式去实现,如:
public class Test { public int func(int a, String b){ //TODO } public int func(){ return func(0,new String("defult")); } ... }
java中类型转换都是动态进行的,类似于c++的std::dynamic_cast。在进行不安全的转换时会抛出异常。
java中char默认占两个字节(默认编码格式为UTF-16)。
java中的abstract关键字,若修饰方法则类似于c++中的virtual,修饰类则与c++中的纯虚类类似,被abstract修饰的类可以有普通方法,需注意,若一个类有abstract方法,则该类必须被abstract修饰。
java中的final关键字,在修饰函数和类时和c++中的final效果一致,防止子类重写和继承。但是java中final可修饰变量,作用类似于c++的const
java中的interface接口类似于c++的纯虚类,但是Java中有纯虚类abstract,但是interface支持多继承,即一个类实现多个接口,但java中无法一个类继承多个父类。接口可以有成员,但是必须修饰为 public static final,接口的方法中只能是抽象方法(需注意,java8以后,接口可以有默认方法(被default修饰的方法),也可以有静态方法)
对于java中的父类继承和接口实现的区别:继承的关系是“是不是”,而实现的关系是“有没有”,比如‘大雁’类,其继承于父类‘鸟’类,实现了接口‘翅膀’
java不支持运算符重载,在进行一些比较的时候应使用方法而非运算符。例如比较两个String内容是否相同应使用equals方法而非直接==,否则比较的是两者栈中的地址值。
java不允许在三元条件运算符中进行赋值操作,如 return (f[x]==x)?x:f[x]=find(f[x]); 该句无法通过编译,需要对f[x]=find(f[x])外层加括号,而c++是支持这种形式的。
java深拷贝方法
- 重写对象即对象成员的clone方法
- 利用序列化反序列化来深拷贝
protected Son deepClone() throws IOException, ClassNotFoundException { Son son=null; //在内存中创建一个字节数组缓冲区,所有发送到输出流的数据保存在该字节数组中 //默认创建一个大小为32的缓冲区 ByteArrayOutputStream byOut=new ByteArrayOutputStream(); //对象的序列化输出 ObjectOutputStream outputStream=new ObjectOutputStream(byOut);//通过字节数组的方式进行传输 outputStream.writeObject(this); //将当前student对象写入字节数组中 //在内存中创建一个字节数组缓冲区,从输入流读取的数据保存在该字节数组缓冲区 ByteArrayInputStream byIn=new ByteArrayInputStream(byOut.toByteArray()); //接收字节数组作为参数进行创建 ObjectInputStream inputStream=new ObjectInputStream(byIn); son=(Son) inputStream.readObject(); //从字节数组中读取 return son; }
- 通过
org.apache.commons
中的工具类BeanUtils
和PropertyUtils
进行对象复制。
java中所有对象都继承自顶级类Object,Object中存在一些通用方法可以被重写,如toString(),hashCode(),equals(),还有一些不可重写方法如wait,notify等和synchronized相关。
需注意,一般对POJO来说,需要重写toString(),hashCode(),equals(),重写toString是为了方便打印调试;重写hashCode是为了为了配合基于散列的集合一起正常运行,如HashSet,Map等;重写equals是为了方便根据需求判断对象是否相等,同时也避免了哈希冲突所带来的影响,即若两者撞了哈希,可以再通过equals去分辨。
Object常用方法如下:
.toString() 返回String对象,默认返回对象地址,可重写。
.equals() 默认比较两个引用变量是否指向同一个对象(内存地址),可重写。
.hashCode() 将与对象相关的信息映射成一个哈希值,默认的实现hashCode值是根据内存地址换算出来,可重写。
.clone() 就如上一条内容提到的,java无法用=直接拷贝,因此可用使用clone去拷贝对象,clone默认是浅拷贝。需注意:
- 实现Cloneable接口,这是一个标记接口,自身没有方法,这应该是一种约定。调用clone方法时,会判断有没有实现Cloneable接口,没有实现Cloneable的话会抛异常CloneNotSupportedException。
- 覆盖clone()方法,可见性提升为public。
.getClass() 返回此 Object 的运行时类,常用于java反射机制。
.wait() .notify()详见java并发锁
java中的引用分为强、软、弱、虚
- 强引用:即java普通的引用,存在于栈上,只要有强引用指向的内存空间就不会被gc回收
- 软引用 :只有在内存空间不足时gc才会回收弱引用指向的内存空间。例子:SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
- 弱引用:只要遇到gc就会被回收。例子:WeakReference<M> m = new WeakReference<>(new M());
- 虚引用:一般不使用,在jvm底层中使用,管理堆外内存。
java中的对象和c++对比起来就像是所有都是以引用类型存在,但是需要额外注意一点
c++中的引用对象参数在函数中使用=赋值之后,在外层的主函数的参数也会改变,这是因为c++支持运算符重载,=实际上是拷贝,而java的=是纯粹的赋值,相当于改变了引用自身的值。
java支持内部类,即在类的内部再声明一个类:
public class OuterClass { class InnerClass { } static class StaticInnerClass { } public static void main(String[] args) { // 在静态方法里,不能直接使用OuterClass.this去创建InnerClass的实例 // 需要先创建OuterClass的实例o,然后通过o创建InnerClass的实例 // InnerClass innerClass = new InnerClass(); OuterClass outerClass = new OuterClass(); InnerClass innerClass = outerClass.new InnerClass(); StaticInnerClass staticInnerClass = new StaticInnerClass(); outerClass.test(); } public void nonStaticMethod() { InnerClass innerClass = new InnerClass(); System.out.println("nonStaticMethod..."); } }
正如上例代码,内部类创建的时候需先有外部类对象,然后调用外部类.new方法创建,静态内部类可直接创建。
java中Arrays.sort()使用的排序算法:对于输入数组类型为基础类型时使用双轴快速排序;对于输入数组类型为包装类型时使用Timsort排序(归并+二分插入)算法
java.nio提供了非阻塞IO机制,主要由Channel通道、Buffer缓冲区、Selector选择器组成。
NIO和传统IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。传统IO线程会阻塞在read和write上,直到数据被完全读取或写入,而NIO从缓冲区读写,无需等待,无数据时可以去干其他事情,因此在处理网络传输时效率较高。
Java中的基本数据类型都有对应的封装 ,当基本数据类型和其对应的封装类型进行以下操作时,编译器会自动进行装箱(将基础类型转化为包装类型)和拆箱(将包装类型转化为基础类型)
- 赋值操作(装箱或拆箱)
- 进行加减乘除混合运算 (拆箱)
- 进行>,<,==比较运算(拆箱)
- 调用equals进行比较(装箱)
- ArrayList、HashMap等集合类添加基础类型数据时(装箱)
补充:Byte、Short、Integer、Long包装类均存在一字节缓存[-128,127],而Character的缓存范围为[0,127]
Integer内部有缓存Integer cache,其下限是-128,上限默认127(即一字节) ,当需要new范围的Integer时,会从缓存中直接取出使用。详见下例
Integer test1 = 99; Integer test2 = 99; System.out.println(test1==test2); test1 = 256; test2 = 256; System.out.println(test1==test2);
运行结果:
可见在缓存范围内创建Integer使用的是缓存区内的同一对象。
需注意,仅在装箱时会使用缓存,直接new对象是不会使用缓存的。
String相关:
String的底层是final char[](JDK9之后为final byte[]),因此String是不可变的,即无法修改String的内部对象也无法修改String的大小,只能改变其指向(重新给予一个String对象)。也正是因为其不可变,让其拥有了线程安全的性质。
需注意,在JDK9之后,String的底层由char[]变为byte[],这样做是为了节省空间提升效率,即当String中只有Latin-1字符时,其按照1字节的规格进行分配内存,否则按照2字节分配。
String str = "123"; String str = new String("123");
以上两种创建方式的区别为,第一种会先从字符串常量池中寻找,若有可用对象则直接用无需再创建,若没有则创建并存入字符串常量池;第二种则会直接在堆中创建,并依然会向字符串常量池存入。可用理解为,只要程序中出现 "xxxx" 字面量,就会向字符串常量池创建String对象储存,因此第二种方式可能产生两个String对象,一个在堆中,一个在常量池中。
有关String的不可变性
String底层的char或byte是final修饰的,因此无法修改。String是线程安全的
实现可变String方法
StringBuilder是一个可变的字符串类,我们可以把它看成是一个容器。常用函数有.append()添加数据并返回自身引用、.reverse()反转容器内容。需注意StringBuilder线程不安全!
StringBuffer的内容和长度同样,都是可以改变的,功能和StringBuilder类似,但是StringBuffer 是线程安全的,内部使用 synchronized 进行同步,因此性能较低。
前面有提过,jdk8支持接口默认方法,即接口提供一个已实现的方法,不用实现接口的类去额外提供实现方法。
java中不存在多继承(一个子类继承多个父类),但是存在多实现(一个类实现多个接口),对于多个接口存在相同的默认方法,或接口的默认方法与本类或父类方法同名的情况遵循如下规则:
- 类优先。如果本类中(或父类)提供了一个具体方法符合签名,则同名且具有相同参数列表的接口中的默认方法会被忽略;
- 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名且参数列表相同的方法 (顺序和类型都相同) ,则必须覆盖这个方法来解决冲突 (不覆盖编译器不会编译);
持续记录中...