首页 文章 精选 留言 我的

精选列表

搜索[java],共10000篇文章
优秀的个人博客,低调大师

Java LinkedHashMap类源码解析

LinkedHashMap继承了HashMap,他在HashMap的基础上增加了一个双向链表的结构,链表默认维持key插入的顺序,重复的key值插入不会改变顺序,适用于使用者需要返回一个顺序相同的map对象的情况。还可以生成access-order顺序的版本,按照最近访问顺序来存储,刚被访问的结点处于链表的末尾,适合LRU,put get compute merge都算作一次访问,其中put key值相同的结点也算作一次访问,replace只有在换掉一个键值对的时候才算一次访问,putAll产生的访问顺序取决于原本map的迭代器实现。 在插入键值对时,可以通过对removeEldestEntry重写来实现新键值对插入时自动删除最旧的键值对 拥有HashMap提供的方法,迭代器因为是通过遍历双向链表,所以额外开销与size成正比与capacity无关,因此选择过大的初始大小对于遍历时间的增加没有HashMap严重,后者的遍历时间依赖与capacity。 同样是非线程安全方法,对于LinkedHashMap来说,修改结构的操作除了增加和删除键值对外,还有对于access-order时进行了access导致迭代器顺序改变,主要是get操作,对于插入顺序的来说,仅仅修改一个已有key值的value值不是一个修改结构的操作,但对于访问顺序,put和get已有的key值会改变顺序。迭代器也是fail-fast设计,但是fail-fast只是一个调试功能,一个设计良好的程序不应该出现这个错误 因为HashMap加入了TreeNode,所以现在LinkedHashMap也有这个功能 以下描述中的链表,若无特别说明都是指LinkedHashMap的双向链表 先来看一下基本结构,每个键值对加入了前后指针,集合加入了头尾指针来形成双向链表,accessOrder代表链表是以访问顺序还是插入顺序存储 static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after;//增加了先后指针来形成双向链表 Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } /** * The head (eldest) of the doubly linked list.头部 */ transient LinkedHashMap.Entry<K,V> head; /** * The tail (youngest) of the doubly linked list.尾部 */ transient LinkedHashMap.Entry<K,V> tail; //true访问顺序 false插入顺序 final boolean accessOrder; 然后是几个内部方法。linkNodeLast将p连接到链表尾部 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) head = p;//原本链表为空则p同时为头部 else { p.before = last; last.after = p; } } transferLinks用dst替换src private void transferLinks(LinkedHashMap.Entry<K,V> src, LinkedHashMap.Entry<K,V> dst) { LinkedHashMap.Entry<K,V> b = dst.before = src.before; LinkedHashMap.Entry<K,V> a = dst.after = src.after; if (b == null) head = dst; else b.after = dst; if (a == null) tail = dst; else a.before = dst; } reinitialize在调用HashMap方法的基础上,将head和tail设为null void reinitialize() { super.reinitialize(); head = tail = null; } newNode生成一个LinkedHashMap结点,next指向e,插入到LinkedHashMap链表末端 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);//新建一个键值对,next指向e linkNodeLast(p);//p插入到LinkedHashMap链表末端 return p; } replacementNode根据原结点生成一个LinkedHashMap结点替换原结点 Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) { LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p; LinkedHashMap.Entry<K,V> t = new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);//生成一个新的键值对next是给出的next参数 transferLinks(q, t);//用t替换q return t; } newTreeNode生成一个TreeNode结点,next指向next,插入到LinkedHashMap链表末端 TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) { TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);//生成一个TreeNode,next指向参数next linkNodeLast(p);//p插入到LinkedHashMap链表末端 return p; } replacementTreeNode根据结点p生成一个新的TreeNode,next设为给定的next,替换原本的p TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p; TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next); transferLinks(q, t);//根据结点p生成一个新的TreeNode,next设为给定的next,替换原本的p return t; } afterNodeRemoval从LinkedHashMap的链上移除结点e void afterNodeRemoval(Node<K,V> e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.before = p.after = null; if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; } afterNodeInsertion可能移除最旧的结点,需要evict为true同时链表不为空同时removeEldestEntry需要重写 void afterNodeInsertion(boolean evict) { LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) {//removeEldestEntry需要重写才从发挥作用,否则一定返回false K key = first.key;//移除链表头部的结点 removeNode(hash(key), key, null, false, true); } } afterNodeAccess在访问过后将结点e移动到链表尾部,需要Map是access-order,若移动成功则增加modCount void afterNodeAccess(Node<K,V> e) { LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) {//Map是access-order同时e不是链表的尾部 LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null)//将结点e从链表中剪下 head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p;//结点e移动到链表尾部 ++modCount;//因为有access-order下结点被移动,所以增加modCount } } 构造函数方面,accessOrder默认是false插入顺序,初始大小为16,负载因子为0.75,这里是同HashMap。复制构造也是调用了HashMap.putMapEntries方法 containsValue遍历链表寻找相等的value值,这个操作一定不会造成结构改变 public boolean containsValue(Object value) { for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {//检查同样是根据LinkedHashMap提供的链表顺序进行遍历 V v = e.value; if (v == value || (value != null && value.equals(v))) return true; } return false; } get方法复用HashMap的getNode方法,若找到结点且Map是访问顺序时,要将访问的结点放到链表最后,若没找到则返回null。而getOrDefault仅有的区别是没找到时返回defaultValue public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null)//复用HashMap的getNode方法 return null; if (accessOrder) afterNodeAccess(e);//access-order时将e放到队尾 return e.value; } public V getOrDefault(Object key, V defaultValue) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return defaultValue;//复用HashMap的getNode方法,若没有找到对应的结点则返回defaultValue if (accessOrder) afterNodeAccess(e);//access-order时将e放到队尾 return e.value; } clear方法在HashMap的基础上要把head和tail设为null public void clear() { super.clear(); head = tail = null; } removeEldestEntry在put和putAll插入键值对时调用,原本是一定返回false的,如果要自动删除最旧的键值对要返回true,需要进行重写。比如下面这个例子,控制size不能超过100 private static final int MAX_ENTRIES = 100; protected boolean removeEldestEntry(Map.Entry eldest) { return size() > MAX_ENTRIES; } 下面两个方法和HashMap相似,返回key的Set和value的Collection还有返回键值对的Set,这个是直接引用,所以对它们的remove之类的修改会直接反馈到LinkedHashMap上 public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new LinkedKeySet(); keySet = ks; } return ks;//返回key值的set } public Collection<V> values() { Collection<V> vs = values; if (vs == null) { vs = new LinkedValues(); values = vs; } return vs;//返回一个包含所有value值的Collection } public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;//返回一个含有所有键值对的Set } 检查HashMap的putVal方法,我们可以看到在找到了相同key值并修改value值时会调用afterNodeAccess,对于access-order会改变结点顺序 if (e != null) { // 找到了相同的key则修改value值并返回旧的value V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }

优秀的个人博客,低调大师

Java 面向对象 之 封装

http://www.verejava.com/?id=16992746970636 /** 测试 Person 类 步骤: 1. 如果要访问Person类必须, 先实例化一个 Person的对象 2. 然后通过返回的对象引用名 去访问该对象在类当中定义的 属性和方法 比如 对象引用名.name 对象应用名.age 对象引用名.say() */ public class TestPerson { public static void main(String[] args) { //对象的实例化 //其中 new Person(); 就是实例化Person对象, 返回一个Person的引用 //p 就是指向 new Person()在堆中生成的对象的一个引用名 Person p = new Person(); //用对象引用名 p 访问属性和方法 p.name = "李明"; p.age = 22; p.say();//李明开始自我介绍了 Person p2 = new Person(); p2.name = "王强"; p2.age = 23; p2.say();//王强开始自我介绍 //把p2指向的Person对象的地址赋值给p,这个时候p 指向的是 p2的对象 //这个时候开始生成的 李明 Person对象已经没有引用指向它, 属于垃圾对象 //当 GC来扫描的时候就会自动回收该对象 p = p2; p.say(); //注意: //如果只是声明一个空的 Person对象 ,而没有实例化, 这个时候访问属性和方法的 //时候编译期能通过,但是在运行期会报 NullPointerException 空指针异常 //Person p3=null; //p3.name="张胜"; //p3.age=24; //p3.say(); } } /* 1. 定义一个Person类 类由属性和方法组成 2. Person类当中有两个属性 :姓名,年龄 3. Person类区别于动物的一个重要方法是:说话 */ class Person { String name; int age; public void say() { System.out.println("我的名字叫:" + name + ",今年:" + age + "岁"); } } http://www.verejava.com/?id=16992746970636

优秀的个人博客,低调大师

Java String类源码解析

String直接继承Object 含有一个char[] value,还有一个int hash默认值为0 new String()的构造产生的是一个值为””的字符数组 String(char value[], int offset, int count)当count=0且offset<=value.length时构造一个值为””的字符串。offset>0且offset+count<=value.length时复制该部分子串。其余情况都会抛错。 字符数据类型是一个采用UTF-16编码表示Unicode代码点的代码单元。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。而length返回的是UTF-16下的代码单元的数量,而codePointCount返回的是代码点的数量。对于大部分人工输入的字符,这两者是相等的,会出现length比codePointCount长的通常是某些数学或者机器符号,需要两个代码单元来表示一个代码点。 对于返回char[]的方法,底层调用的是System.arraycopy方法,这也是高效的数组拷贝函数。 getBytes方法会调用StringCoding.encode返回序列化后的byte[] 关于String a == String b的判断,是指a和b指向内存中的同一个对象,凡是new String初始化的对象,都不会产生a==b的情况,因为他会新开辟一个对象空间,然后复制value的值,仅当b=a初始化时a==b成立。 public static void main(String args[]) { String a, b; a = "123"; b = "123"; System.out.println(a==b);//true a = "123"; b = new String("123"); System.out.println(a==b);//false a = new String("123"); b = new String("123"); System.out.println(a==b);//false a = "123"; b = new String(a); System.out.println(a==b);//false a = new String("123"); b = a; System.out.println(a==b);//true } 而a.equals(b)先判断a == b是否成立,再判断b是否是String类,然后逐个比较value数组的值是否相等。equalsIgnoreCase在此基础上忽略大小写的区别 a.compareTo(b)比较a和b第一个不相等字符的差值,若都相等则比较长度差值。compareToIgnoreCase多一个忽略大小写的区别。regionMatches(int toffset, String other, int ooffset, int len)则是比较a从toffset开始和other从ooffset开始长度为len的部分是否相等。 startsWith(String prefix, int toffset)字符串从tooffset位置开始和prefix是否相等。endsWith(String suffix)字符串结尾和suffix等长部分是否相等。 hashCode()调用时,若hash值为0且字符串长度不为0,则要计算hash值,方法是value数组化为31进制 indexOf是返回第一个出现的指定值的位置,可以通过fromIndex来指定开始查找的位置,而indexOfSupplementary是忽略大小写的该方法。lastIndexOf则是从尾部开始查找最后一个。 substring根据指定的位置返回一个新的子字符串,若指定位置不符合原字符串的长度,则抛错。 a.concat(String str)新建一个字符串内容是a+str并返回,不会修改a原本的值 public static void main(String args[]) { String a, b; a = "123"; b = "123"; a.concat(b); System.out.println(a);//123 System.out.println(a.concat(b));//123123 a = a.concat(b); System.out.println(a);//123123 } replace(char oldChar, char newChar)生成一个新的字符串,将原字符串中的oldChar字符全部替换为newChar,不会改变原字符串的值。replaceAll(String regex, String replacement)和前一个方法相比,参数regex是正则表达式,其余相同。 public static void main(String args[]) { String a; a = "12131"; a.replace("1", "a"); System.out.println(a);//12131 a = a.replace("1", "a"); System.out.println(a);//a2a3a } split(String regex, int limit)将字符串按照给定的正则表达式分割为字符串组,limit是分割产生的数组最大数量,对于多余部分不做分割全部保留在最后一个字符串中。 public static void main(String args[]) { String a; a = "1,2,3,4"; String[] b = a.split(","); for(String t : b){ System.out.print(t + " ");//1 2 3 4 } System.out.println(""); String[] c = a.split(",", 3); for(String t : c){ System.out.print(t + " ");//1 2 3,4 } } toCharArray()复制出一个新的char[]而不是直接返回value trim()生成一个新的字符串,去掉头部的所有空格 public native String intern()这个方法的作用是在常量池当中寻找是否已经存在该字符串,若已存在则返回该引用,若不存在则在常量池新建。从上面的源码分析中,我们可以看出String的所有操作都是返回一个新的字符串,对自身是没有修改的,String被设计为一个不可变的final对象,理由有以下几点: 1. 字符串常量池的需要。字符串常量池的诞生是为了提升效率和减少内存分配。、因为String的不可变性,常量池很容易被管理和优化。 2. 安全性考虑。正因为使用字符串的场景如此之多,所以设计成不可变可以有效的防止字符串被有意或者无意的篡改。(通过反射或者Unsafe直接操作内存的手段也可以实现对所谓不可变String的修改)。 3. 作为HashMap、HashTable等hash型数据key的必要。因为不可变的设计,jvm底层很容易在缓存String对象的时候缓存其hashcode,这样在执行效率上会大大提升。 public static void main(String args[]) { String a, b; a = "123"; b = new String(a).intern(); System.out.println(a == b);//true a = "12" + "3"; b = "123"; System.out.println(a == b);//true a = "12" + "3"; b = new String("123"); System.out.println(a == b);//false a = "12" + "3"; b = new String("123").intern(); System.out.println(a == b);//true a = new String("123"); b = a.intern(); System.out.println(a == b);//false } 从上面一段代码的运行结果我们可以看到,intern()会从常量池寻找指定的字符串,指向同一个常量池对象的时候,a==b就是成立的。这里说明一下最后一个case,首先常量池存在了”123”,然后a获得的引用是另一个”123”(因为是new String得到的对象),而b得到的是常量池中第一个”123”的引用,所以a!=b。对于字符串相加的操作"12" + "3",操作过后常量池内会有3个字符串,"12" "3" “123” 个人GitHub地址: https://github.com/GrayWind33

优秀的个人博客,低调大师

java.lang.IndexOutOfBoundsException and drawPosText

package com.example.canvastest; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; /** * Created by 700 on 2018/7/31. */ public class DrawText_DrawPosText extends View{ private Paint mTextPaint; private float[] offset = new float[10]; public DrawText_DrawPosText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public DrawText_DrawPosText(Context context, AttributeSet attrs) { super(context, attrs); } public DrawText_DrawPosText(Context context) { super(context); } private void initView(){ for(int i = 0; i<10; i+=2){ offset[i] = 100; offset[i+1] = 250+150*i/2; } mTextPaint = new Paint(); mTextPaint.setColor(getResources().getColor( android.R.color.holo_blue_light )); mTextPaint.setTextSize(20);//设置文字大小 // mTextPaint.setTextAlign(Paint.Align.CENTER);//设置居中 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); initView(); canvas.drawText("hehehehehedadadadadadadada",100,100,mTextPaint); canvas.drawPosText("hehehehehedadadadadadadada",offset,mTextPaint); } } 郁闷。。报错的原因是数组超范围了。。 还以为是for语句写错了,看了半天。。。没错啊。。。 后来聚焦在了drawPosText()上面,觉得不太对劲,看了下源码,额。。。。。。, 意思是干嘛呢,drawPosText()不是第一个参数是字符串嘛,drawPosText()这个方法就是把这个字符串拆成一个一个字符,按顺序对应好第二个参数的位置放好。。。,也就是说我第二个参数offset这个数组长度是10,即包含着五个xy坐标对,只能描述五个位置,所以第一个参数只能是一个五字符的字符串,不然就跟数组不匹配,便会报错了。。。 喏,第一个参数改成是一个五字符的字符串: 也便成功运行了:

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册