ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

java基本数据类型与集合

2022-06-21 16:33:57  阅读:81  来源: 互联网

标签:java String int 数据类型 value 对象 key 集合 数组


本篇是本人二次学习java的一些总结,相比第一次只会敲不知道咋回事,有了一些进步,将这些分享给大家,如果有不对的地方,还望大家指出,谢谢各位。
学习笔记有些长,还望海涵。

目录


1.编程入门

1.1.入门概述

计算机分为硬件与软件

程序设计

定义:创建了(或开发)软件。软件包含了指令,告诉计算机做什么。

应用场景:软件遍布周围。

程序设计语言

软件开发人员在成为程序设计语言的强大工具的帮助下创建软件。

1.2 软件开发介绍

软件:按照特定顺序组织的计算机数据和指令的集合。有系统软件和应用软件之分。(系统软件:windo,linux,(操作系统))

人机交互方式:

图形化界面:简单直观,使用容易接受,容易上手

命令行方式:例如:win+r。

常用的DOS命令:

dir:列出当前目录下的文件和文件夹

md:创建目录

rd:删除目录

cd:进入指定目录

cd.:退回到上一级目录

cd:退回到根目录

del:删除文件

exit:退出dos命令行

1.3 java语言的特点

1.面向对象

​ 两个基本概念:类、对象

​ 三大特性:封装、继承、多态

  1. 健壮性

    ​ 吸收

  2. 跨平台性

2 基本数据类型与对象

基本数据类型 又名 基本类型又叫内置数据类型,对象又叫引用数据类型。
基本类型共8种,其他的都是对象类型

2.1 Java基本数据类型

数据类型 比特位(1字节B=8位b) 数据 数值范围
byte 8位 有符号的以二进制补码表示的整数 -128-127
short 16位 有符号的以二进制补码表示的整数 -215-215-1
int 32位 有符号的以二进制补码表示的整数 -231-231-1
long 64位 有符号的以二进制补码表示的整数 -263-263-1
float 32位 符合IEEE 754标准的浮点数 -3.4 E+38 ~ 3.4 E+38
double 64位 符合IEEE 754标准的浮点数 -1.7 E -308~1.7 E+308
boolean 1位(规定一般32位处理,如果数组8位) true or flase
char 16位 Unicode(无符号)字符

​ 在计算机中,整数型使用二进制方式表示:而每一个整数型的第一个二进制都是作为正负符号。 0=正 1=负

  • 基本数据类型在栈中进行分配,也就是说存储了实际的数值;

  • 对象类型则是在堆中进行分配,它实际存存储的其实是引用地址;(但是数据的引用在栈中);

基本类型之间的传值是值传递, 对象之间的传值是 ?

关于java对象究竟是值传递还是引用传递,一直是众说纷纭,各自都有各自的理由,

值传递认为 你将栈里存的引用(就是堆中的对象实例地址)复制了一份交给了方法体的局部变量,而不是直接传递

引用传递认为 在调用函数时将实际参数的地址直接传递到函数中,而传递过来的地址还是与之前的地址指向同一个值,那么要是修改了这个参数,就会影响这个值的改变

最后,我选择了主流观点 对象之间的传值是值传递,来看这么一段代码:

 public static void main(String[] args) {
        // TODO Auto-generated method stub
        String str = "data1";
        String str2 = new String("data2");
        StringBuffer buffer = new StringBuffer("data3");
        System.out.println("1 str:=" + str + " str2:=" + str2+ " buffer:=" + buffer.toString());
        //1 str:=data1 str2:=data2” buffer:=data3
        dealData(str, str2, buffer);
        System.out.println("2 str:=" + str + " str2:=" + str2 + " buffer:=" + buffer.toString());
        //2 str:=data1 str2:=data2” buffer:=data3123
    }
    private static void dealData(String str, String str2, StringBuffer buffer) {
        str += "123";
        str2 += "123";
        buffer.append("123");
        System.out.println("nerborn: str:=" + str + " str2:=" + str2 + " buffer:=" + buffer.toString());
        //nerborn: str:=data1123 str2:=data2123 buffer:=data3123
    }

我们将结果放在一起

  1      str:=data1    str2:=data2”   buffer:=data3
nerborn: str:=data1123 str2:=data2123 buffer:=data3123
  2      str:=data1    str2:=data2”   buffer:=data3123

str与str2,因为String内部的value数组是被final修饰的,所以在进行字符串拼接会创建一个新对象,也就是说 在dealData中,进行字符串拼接后就产生了一个新对象,也就是说这时候的str与str2已经指向了堆里一个新对象实例,自然是不会对原本的对象产生修改(如果有不理解的小伙伴可以去了解下jvm与String,我后续也会继续更新的)

接下来我们来看buffer,很多人都觉得buffer就是典型的引用传递,但其实不然,因为这个例子其实有问题,

我们可以换成这样

 private static void dealData(String str, String str2, StringBuffer buffer) {
        str += "123";
        str2 += "123";
        buffer = new StringBuffer("data3123");
        System.out.println("nerborn: str:=" + str + " str2:=" + str2 + " buffer:=" + buffer.toString());
    }

其余均不变,结果为

1        str:=data1    str2:=data2    buffer:=data3
nerborn: str:=data1123 str2:=data2123 buffer:=data3123
2        str:=data1    str2:=data2    buffer:=data3

相信很多小伙伴看到这里就已经明白了,buffer.append方法,本质上会修改对象实例中的value数组,所以看起来我改他也该,

而当我们选择new一个对象时,地址引用发生改变,原对象自然也不再改变,

所以我们可以得出:java对象是值对象,看起来是引用传递,是因为对象所属类中存在改变对象实例数据的方法

2.2 String

在学习String之前我先给大家介绍下==与equals方法:

对于基本数据类型:==是比较值,无法使用equals方法

对于对象: == 是比较引用所指向的地址,equals也是比较引用,原因如下:

Object源码下的equals方法

    public boolean equals(Object obj) {
        return (this == obj);
    }

但String,Date、Integer重写了equals方法,因此实际比较的是值;

String源码下的equals方法

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

总结:先比地址,在比长度,最后遍历欸个比较,不妨看看如下代码:

public static void main(String[] args) {
        // TODO Auto-generated method stub
        int n=3;
        int m=3;        
        System.out.println(n==m); //基本类型值比较,true
        String str = new String("hello");
        String str1 = new String("hello");
        String str2 = new String("hello");        
        System.out.println(str1==str2); //对象引用地址比较,false 
        str1 = str;
        str2 = str;
        System.out.println(str1==str2); //对象赋值后引用地址比较,true
        System.out.println(str1.equals(str2));//String的equals方法重写了,比较的是值,true
        StringBuffer sb = new StringBuffer("123");
        StringBuffer test = new StringBuffer("123");
        System.out.println(sb == test); //对象引用地址比较,false
        System.out.println(sb.equals(test));//StringBuffer的equals方法没有重写,比较内存地址,false
    }

下面进入String的学习:

先贴源码(部分):

ublic final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** 该值用于字符存储。 */
    private final char value[];

    /** 缓存字符串的哈希码 */
    private int hash; // Default to 0

    /**
     * 初始化一个新创建的String对象,使其表示一个空字符序列。请注意,由于字符串是不可变的,因此不需要使用此构造函数
     */
    public String() {
        this.value = "".value;
    }

    /**
     *初始化一个新创建的String对象,使其表示与参数相同的字符序列;换句话说,新创建的字符串是参数字符串的副本。除非需要original	  *的显式副本,否则不需要使用此构造函数,因为字符串是不可变的。
     *
     *参形:原始 - 一个String
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    /**
     *分配一个新的String以便它表示当前包含在字符数组参数中的字符序列。字符数组的内容被复制;随后对字符数组的修改不会影响新创建的      *字符串。
     * 形参:
     *  value – 字符串的初始值
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    
    /**
     * 分配一个新String ,该字符串包含来自字符数组参数的子数组的字符。 offset参数是子数组第一个字符的索引, count参数指定子数	 *组的长度。子数组的内容被复制;随后对字符数组的修改不会影响新创建的字符串。
	 *形参:
	 * value – 作为字符源的数组
	 * offset - 初始偏移量
	 * count - 长度
	 *	
	 *抛出:IndexOutOfBoundsException – 如果offset和count参数索引字符超出value数组的范围
	 *
     */
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
  • String是一个对象,不属于8种基本数据类型,因为对象的默认值是null,所以String的默认值也是null;
  • 但它又是一种特殊的对象,有其它对象没有的一些特性

例如:

  1. new String()和new String(“ ”)都是申明一个新的空字符串,是空串不是null

  2. String="abc"与new String("abc")是不一样的:

    String str1 = "abc";  // 在常量池中
    
    String str2 = new String("abc"); // 在堆上
    

    这里会涉及到jvm,先简单说一下对象存储

对象存储如下

image-20220621144555843

 String="abc"  当直接赋值时,字符串“abc”会被存储在字符串常量池中,只有1份,此时的赋值操作等于是创建0个或1个对象。

​ 如果常量池中已经存在了“abc”,那么不会再创建对象,直接将引用赋值给str1;

​ 如果常量池中没有“abc”,那么创建一个对象,并将引用赋值给str1。

 	 String s0="abc"; 
      String s1="abc"; 
      String s2="a" + "bc"; //如果是常量进行字符串拼接时优化器会直接将拼接完成后的字符串,然后检验字符串常量池,
							//也就是说,此时这行代码实际上并没有创建对象,只是栈上多了一个引用	
      System.out.println( s0==s1 ); //返回true
      System.out.println( s0==s2 ); //返回true

那么,通过new String(“abc”);的形式又是如何呢?答案是1个或2个。

当JVM遇到下述代码时,会先检索常量池中是否存在“abc”,如果不存在“abc”这个字符串,则会先在常量池中创建这个一个字符串。然后再执行new操作,会在堆内存中创建一个存储“abc”的String对象,对象的引用赋值给str2。此过程创建了2个对象。

当然,如果检索常量池时发现已经存在了对应的字符串,那么只会在堆内创建一个新的String对象,并指向,此过程只创建了1个对象。

简单点说:

会先在堆中建立一个空字符串,随后检测内存中是否有括号中的字符串,若有,直接修改空字符串,若无创建一个新的字符串并通过该字符串修改空字符串

 	 String s0 = "abc"; //s0直接指向字符串常量池中的abc,
      String s1 = new String("abc"); //s1指向队中的String实例,String实例指向字符串常量池中的abc,
      String s2 = "a" + new String("bc");//s2指向堆中的String实例,注意此时是对象拼接而成的,
      System.out.println(s0 == s1);//false
      System.out.println(s0 == s2);//false
      System.out.println(s1 == s2);//false

大家可能对s1与s2有些疑惑,简单解释一下:

s1与s2都新new了一个String实例,肯定不一样

详细解释一下:

s1指向队中的String实例,String实例指向字符串常量池中的abc,

s2则涉及到带到对象的字符串拼接,如果字符串拼接中包括字符串,那么会隐式创建一个StringBuilder对象,然后使用append方法将他们拼接到一起,最后执行toString方法,生成新的String对象(最后拼接成的字符串其实是不会放进字符串常量池的)

而StringBuilder的toString方法如下

  @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

所以s1与s2不一致

这里还要提及String的一个方法intern:

存在于class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;

当一个String实例apache调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,

如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于apache的字符串并返回它的引用;参照如下代码:

 public static void main(String[] args) {
      // TODO Auto-generated method stub
      String s0= "apache"; 
      String s1=new String("apache"); 
      String s2=new String("apache"); 
      System.out.println( s0==s1 ); 
      System.out.println( "**********" ); 
      s1.intern(); //s1调用intern方法后,因为没有赋值,所以没有改变引用
      s2=s2.intern(); //把常量池中"kvill"的引用赋给s2 
      System.out.println( s0==s1); //false
      System.out.println( s0==s1.intern() ); //true
      System.out.println( s0==s2 ); //true
  }

必须注意的是:不是“将自己的地址注册到常量池中”了,而是在常量池中增加一个Unicode等于apache的字符串并返回它的引用

  • String的split方法 切割字符串,但是该方法在字符串很大的时候有性能瓶颈,所以如果把String按照某种分隔符拆分成String[]数组时,需要注意String的大小。
  • 关于String是不可变的
    String内部是private final char value[]; 因此String的实例一旦生成就不会再改变了,即修改String的值会产生临时变量。
    所以代码中如果需要经常修改String的值,建议用StringBuffer或者StringBuilder。

2.3 类型转换

在实际应用中,经常需要在不同类型的值之间进行操作,这时就需要进行数据类型的转换。 数据类型转换有两种:

  • 自动类型转换:编译器自动完成类型转换,不需要在程序中编写代码;
    规则:从存储范围小的类型到存储范围大的类型。
    具体规则:byte→short(char)→int→long→float→double.
    例如:byte b= 100;int n = b;
    注意:对于short、char两种类型而言,他们是相同级别的,因此,不能相互自动转换,但是可以强制类型转换
  • 强制类型转换 :强制类型转换,也称显式类型转换,是指必须书写代码才能完成的类型转换。该类类型转换很可能存在精度的损失,所以必须书写相应的代码,并且能够忍受该种损失时才进行该类型的转换。
    转换规则:从存储范围大的类型到存储范围小的类型。
    具体规则为:double→float→long→int→short(char)→byte
    例如:double d = 3.10;int n = (int)d;

扩展 为什么long可以自动转换为float

这是因为long和float的存储方式的不同

我们知道:long占8个字节 表示范围为2^63 —2^63-1

但float不同,

float:单精度浮点数 V=(-1)^s ∗ M ∗ 2^E

浮点数的32位不是简单的直接表示大小,而是按照一定的标准分配的。
  其中第1位,符号位,即S。【所以符号位1表示负数,0表示正数】
  接下来的8位,指数域,即E,为了同时表示正负指数以及他们的大小顺序,这里实际上的数是E-127,比如如果是126,则这里存储的是126-127=-1,
  剩下的23位,小数域,即M,M的取值范围为[0,1)或【1,2)。

如果正是因为指数域的存在,导致float范围发生了巨大的变化

我们可以来算一下float的最大值,

​ 首先必须为正数,第一位为0

​ 指数取最大值,因为1111111111我们不能占用,所以最大值为11111110 = 28-1-127=27

​ 位数有23个1,则

最大为(2-2-23)*2127= (1-2-24)*2128 ≈ 2^128 = 3.40282367+E 38

简单来说float因为存储的方式,可以存储更多的数,存储的大小不是因为比特位数,而是因为存储的结构

扩展 进一步理解精度丢失

正是因为存储结构的不同,所以整数转浮点数肯定会存在精度丢失

int是准确值,而float是精确值,准确转精确当然会精度丢失

因此 :在进行货币等精确计算时应使用BigDecimal。
程序中如果对精度要求不是很高的情况,可以使用float。但精度要求高的情况,要尽量使用double。如果要求更高的精度,则应使用BigDecimal

为什么float被称为精确值,这就是因为尾数的存在,尾数默认23位,加上默认省略的1位就是24位,因此如果int类型的值在2^24以内,float是可以精确表示的,但是当超过这个数的时候就不一定能精确表示了。

举个例子:

float 32 符号位 8位指数位(0-255) 23位尾数位

2.5 转二进制 10.1 科学计数法表示 1.01*2^-1

符号位 为正数 0

指数为 -1+127=126=01111110

小数位 01 后面补0 01000000000000000

那么最终结果为 0 01111110 01000000000000000

2.4 基本类型包装类

  • 1、基本类型在Java的lang包中都有一个相应的包装类,如:Byte,Short,Character,Integer,Long,Float,Double,Boolean,通过这些包装类,我们就可以将基本数据类型包装成类对象。
  • 2、包装类的构造方法:
    所有包装类都可将与之对应的基本数据类型作为参数,来构造它们的实例,例如:Integer i=new Integer(1);
    除Character类外,其他包装类可将一个字符串作为参数构造它们的实例,例如:Integer i=new Integer(“123”);
  • 3、注意事项:
    Boolean类构造方法参数为String类型时,若该字符串内容为true(不考虑大小写),则该Boolean对象表示true,否则表示false;
    当Number包装类构造方法(上面的8种包装类中除了Character和Boolean,都继承了Number)参数为String 类型时,字符串不能为null,且该字符串必须可解析为相应的基本数据类型的数据,否则编译通过,运行时NumberFormatException异常;
  • 4、所有的包装类内部都是final类型的基本数据,所以都是不可变的

扩展 自动装箱与拆箱

自动装箱和拆箱从Java 1.5开始引入,目的是将原始类型值转自动地转换成对应的对象。
在JDK1.5之前,基本类型转换为对象需要程序显示用包装类进行处理,有了装箱和拆箱后就自动了,程序就不需要显示处理了。例如:Integer num = 1;

  • 1、什么是自动装箱和拆箱?
    自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。

  • 2、自动装箱拆箱要点
    自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
    自动装箱是将boolean值转换成Boolean对象,byte值转换成Byte对象,char转换成Character对象,float值转换成Float对象,int转换成Integer,long转换成Long,short转换成Short,自动拆箱则是相反的操作。

  • 3、何时发生自动装箱和拆箱
    自动装箱和拆箱在Java中很常见,比如我们有一个方法,接受一个对象类型的参数,如果我们传递一个原始类型值,那么Java会自动讲这个原始类型值转换成与之对应的对象。
    例如:

ArrayList intList = new ArrayList();
intList.add(1); //自动装箱
intList.add(2); //自动装箱
自动拆箱(unboxing),也就是将对象中的基本数据从对象中自动取出。如下可实现自动拆箱:
Integer i = 10; //装箱
int t = i; //拆箱,实际上执行了 int t = i.intValue();

4、注意事项:
Integer.valueOf(int i) 这个方法,如下:

 public static Integer valueOf(int i) {        
    assert IntegerCache.high >= 127;        
    if (i >= IntegerCache.low && i <= IntegerCache.high)           
   		 return IntegerCache.cache[i + (-IntegerCache.low)];       
    return new Integer(i);    }

对于–128到127(默认是127)之间的值,Integer.valueOf(int i) 返回的是缓存的Integer对象(并不是新建对象,创建这个缓存是为了提高效率)

2.5 数组与集合

Java中的数组Array

Java所有“存储及随机访问一连串对象”的做法
优点:效率高,但容量固定且无法动态改变。
缺点:无法判断其中实际存有多少元素,length只是告诉我们array的容量

String [] arr;
int arr1[];
String[] array=new String[5];
int score[]=new int[3];

也可以直接赋值:

int[] m = { 1, 2, 3 };
String[] strings = { "aaa", "bbb" };

Java中有一个Arrays类,专门用来操作数组array。

Arrays中拥有一组static函数
equals():比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。
fill():将值填入array中。
sort():用来对array进行排序。
binarySearch():在排好序的array中寻找元素。
System. arraycopy():array的复制。

Java中的集合

若撰写程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,array不适用。所以就要用到集合

1、集合分类:

Collection :List、Set || Map :HashMap、HashTable

2、集合 List Set Map的区别

Java中的集合包括三大类,它们是Set、List和Map,它们都处于Java .util包中,Set、List和Map都是接口,它们有各自的实现类。

Set的实现类主要有HashSet和TreeSet;

List的实现类主要有ArrayList;

Map的实现类主要有HashMap和TreeMap。

  • List中的对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,如通过list .get(i)方式来获得List集合中的元素
  • Set中的对象不按特定方式排序,并且没有重复对象。但它的有些实现类能对集合中的对象按特定方式排序,例如TreeSet类,它可以按照默认排序,也可以通过实现java . util .Comparator来自定义排序方式。
  • Map中的每一个元素包含一个键对象和值对象,它们成对出现。键对象不能重复,值对象可以重复。

3、总结一下:

  • Collection 和 Map 的区别:容器内每个为之所存储的元素个数不同。Collection类型者,每个位置只有一个元素。Map类型者,持有 key-value pair,像个小型数据库

  • 各自旗下的子类关系:

    • Collection
      —List:将以特定次序存储元素。所以取出来的顺序可能和放入顺序不同
      ArrayList / LinkedList / Vector,Stack继承自Vector(其中Vector和Stack是线程安全的)
      —Set : 不能含有重复的元素
      HashSet / TreeSet

    Collection集合也有一个专门的操作类Collections,比如Collections. sort(List);//进行排序

    Map( HashTable既不允许 null 键也不允许 null 值但 HashMap 允许任意数量的 null 值和最多一个 null 键。另外HashTable是线程安全的)
    —HashMap
    —HashTable
    —TreeMap

常用方法:

list删除 建议使用迭代器删除

Iterator<String> it = list.iterator();
while(it.hasNext()){
    String x = it.next();
    if(x.equals("del")){
        it.remove();
    }
}

使用list的remove方法会报ConcurrentModificationException错误。

如果只删除一个元素的话也可以for循环然后找到要删除的元素,调用list. remove(obj)的方式进行删除,但实际一般不采用

//方式1:for循环遍历list
for(int i=0;i<list.size();i++){
    if(list.get(i).equals("del"))
        list.remove(i);
}
//这种方式的问题在于,删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用。
方式2:增强for循环
for(String x:list){
    if(x.equals("del"))
        list.remove(x);
}
//这种方式的问题在于,删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出。但是删除完毕马上使用break跳出,则不会触发报错。

list排序:

  • 运用collections .sort方法

如果List集合中的元素内容只有一个数据,就直接比较即可,前提是保证这个数据对应的类中有CompareTo方法

collections.sort(集合对象,new compartor(){
	 @Override
            public int compare(String o1, String o2) {
                return o1.equals(o2)?1:-1;
            }
})
  • 如果List集合中的元素内容有多个数据,就需要这个元素类型必须实现Comparable接口,并重写CompareTo方法,在此方法中指定排序原则
public Student implements compartable<Student>{
    public int compareTo(Student student) {
        return student.name.equals(this.name)?1:-1;
    }
}

HashMap put get

总结:

  • put

    先判断是否map是否为空,若为空则运用inflateTable开辟内尺寸空间,

    ​ 判断key值是否存在,不存在直接存储到数组起始位置(若已存在元素则存储在链接的链表末尾);

    ​ 若存在,则计算hash值,根据位运算indexFor(内部为位运算hash&(table.length-1)求出数组下标, 用for循环进行遍历, 若存在相同元素(key hashcode均相等),则直接替换(value);

    ​ 若不存在,调用addEntry加到链表的末尾

  • get(与put逻辑基本一致)

    先判断key值是否为空,

    ​ 若为空,直接调用方法getForNullKey()去起始位置的链表找;

    ​ 若不为空,计算hash值,根据位运算indexFor求出数组下标,使用for循环进行遍历,

    ​ 若找到相同元素(key hashcode均相等),将其输出

    ​ 若不存在,返回null

详情:

  • put操作,从英文来看,叫赋予操作,可以理解为:添加或修改操作
    首先会判断map是否为空,如果为空就会先开辟内存空间;
    接着开始存储(或修改)元素,若未传入key,则说明是一个单纯的添加操作,会直接存储在数组下标位0处(若已存在元素则添加到对应链表中),
    如果传入key,根据key计算hash值,再根据生成的哈希值计算出数组存储的下标,接着调用for循环遍历该下标处的链表结构,如果hash值与key值均相同,则修改该处的value值,
    否则就是没有找到,则在该链表的末尾添加一个新的entry,并修改原先末尾entry的末尾指向它;
public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,
        //此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);//获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i);//新增一个entry
        return null;
    }


inflateTable这个方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
        /**此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,
        capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1 */
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

roundUpToPowerOf2中的这段处理使得数组长度一定为2的次幂,Integer.highestOneBit是用来获取最左边的bit(其他bit位为0)所代表的数值.
private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

hash函数

/**这是一个神奇的函数,用了很多的异或,移位等运算
对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀*/
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
以上hash函数计算出的值,通过indexFor进一步处理来获取实际的存储位置

h&(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为index=2。位运算对计算机来说,性能更高一些(HashMap中有大量位运算)

/**
     * 返回数组下标,实际上就是取余(摸)运算的位运算写法
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

再来看看addEntry的实现:

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }


通过以上代码能够得知,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。

get操作 则就是输出,查询操作
第一步 ,判断key是否为空,为空就输出null,
若不为空,则调用hash(key)方法获取哈希值,运用indexFor(hash)方法找到数组下标,使用for循环遍历链表,找到则输出,否则输出为空。(基本与put操作一致)

 public V get(Object key) {
     //如果key为null,则直接去table[0]处去检索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }
 
final Entry<K,V> getEntry(Object key) {
            
        if (size == 0) {
            return null;
        }
        //通过key的hashcode值计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && 
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }    

数组与集合区别

1、数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),

​ 而JAVA集合可以存储和操作数目不固定的一组数据, 所有的JAVA集合都位于 Java. util包中, JAVA集合只能存放引用类型的的数据,不能存放基本数据类型。

2、数组与集合的区别:
有人想有可以自动扩展的数组,所以有了List ;

​ 有的人想有没有重复的数组,所以有了set ;

​ 有人想有自动排序的组数,所以有了TreeSet,TreeList,Tree

-》而几乎有有的集合都是基于数组来实现的. 因为集合是对数组做的封装,所以,数组永远比任何一个集合要快
-》但任何一个集合,比数组提供的功能要多:元素类型不声明;动态大小;数字没法只读,集合提供了ReadOnly方法可以返回只读

扩展 常见注意事项

1. java中Collection和Collections有什么区别呢?

java.util. Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

java .util. Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。比如:
Collections. sort(list) //对list进行排序
Collections. sort是按照默认规则进行排序,也可以某种特定规则进行排序的话可以实现Comparator接口。

//在java8中可以使用Lanbda表达式
Collections.sort(lists, (Studen a, Studen b)->{
    return b.compareTo(a);
})
//或者
Collections.sort(lists, (Studen a, Studen b) -> b.compareTo(b));
//或者
Collections.sort(lists, (a, b) -> b.compareTo(a));
//【备注】此时需要在Student对象中实现一下compareTo方法(方法的内部逻辑就是上面的compare内部的方法)

2.List元素要按照对象的属性进行排序,该如何实现呢?

若只有一个元素,直接调用Collections .sort方法创建内部类Comparator指定排序规则

若有多个元素,在实体类实现compartable接口的comparto进行实现

3.集合的内部源码是如何实现的(比如HashMap或者TreeSet等)?

HashMap原理关键点:+

使用“链地址法”解决哈希冲突,内部使用数组(Entry数组)实现,每个位置是包含一个key-value键值对的Entry基本组成单元,放入元素的过程主要有两步:
1.通过hashcode找到数组中的某一元素,如果当前位置无元素的话直接放在当前位置
2.如果当前位置有元素的话,通过key的equals方法进行判断,如果返回true的话直接更新当前位置,如果false的话需遍历链表,存在即覆盖,否则新增,此时链表时间复杂度为O(n)
读取元素的时候跟上面步骤类似,首先根据hashcode找到数组位置,如果该位置无链表的话直接返回;如果该位置有链表的话需遍历链表,然后通过key对象的equals方法逐一比对查找

HashSet实现原理:

HashSet是一个没有重复元素的集合,HashSet其实是由HashMap实现的,HashMap中保存的是键值对,然而我们只能向HashSet中添加key,原因在于
HashSet的Value其实都是同一个对象,这是HashSet添加元素的方法,可以看到辅助实现HashSet的map中的value其实都是Object类的同一个对象。

TreeMap实现原理:

内部使用红黑树来进行排序的,每个元素是Entry的结构,如下:

static final class Entry<K,V> implements Map.Entry<K,V> {
       K key;
       V value;
       // 左孩子节点
       Entry<K,V> left = null;
       // 右孩子节点
       Entry<K,V> right = null;
       // 父节点
       Entry<K,V> parent;
       // 红黑树用来表示节点颜色的属性,默认为黑色
       boolean color = BLACK;
	   //更多次数不再描述

为了维护数据的唯一性。再存入数据的时候,他们会调用元素中实现的 Comparable 的 compareTo()方法或者集合本身创建的时候传入了迭代器Comparator,这个时候会调用compare()方法【一般推荐我们的key本身或者父类已经实现了Comparable接口】。
【备注】Comparable和Comparator都是用于比较数据的大小的,实现Comparable接口需要重写compareTo方法,实现Comparator接口需要重写compare方法,这两个方法的返回值都是int,用int类型的值来确定比较结果。

TreeSet实现原理:

TreeSet也是没有重复的排序集合,TreeSet也是由TreeMap实现的,这个跟HashSet的实现方式是类似的。

4. map是否是有序的?

HashMap是无序的,TreeMap是有序的

5. equals()相等的两个对象,hashcode是否相同,为什么?

如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。

详情请看下一道

6.为什么要重写equals,怎么重写equals,重写equals时还必须重写hashcode方法

重写equals是为了比较变量的值而不是地址,java Object是所有类的源头,但他的equals方法是比较地址的,所以我们需要重写(String ,date,Integer已重写)

怎么重写?以String为例子,先比较地址,地址一样肯定值也一样,在比长度,最后挨个比较

为什么还要重写hashcode方法?

来看如下的一段话:

Java对于eqauls方法和hashCode方法是这样规定的:

(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;

(2)如果两个对象的hashCode相同,它们并不一定相同。

当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。

我的理解是这样的,这其实是一个约定(其实我想说规定,但人家没说是强制),如果我们只使用equals来进行比较,效率会直线下降, 现在hashcode广泛应用于java中,为了不违背这个约定,以免降低性能,我们最好将其重写。

Object的hash方法注释中也提到了

7. 为什么遍历HashMap不用keySet来进行遍历?

效率问题,先了解一下HashMap的方法

  • values()方法 返回的是V值集合

  • keySet () 返回的是K值集合,是一个Set集合对象;

  • entrySet()返回的是K-V值组合集合。

在使用keySet方法的时候,我们只会存放key值,进行了一次遍历,

在输出的时候我们需要使用map.get(key)方法再进行一次遍历,所以执行了两次遍历;

entrySet() 存放的是map.entry<T,T>,在进行遍历的时候直接输出即可。

标签:java,String,int,数据类型,value,对象,key,集合,数组
来源: https://www.cnblogs.com/yfmw/p/yfmw_java01.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有