Post

[JAVA] HashSet

[JAVA] HashSet

문자열 해시 코드

해시 인덱스를 사용하면 데이터의 값을 배열의 인덱스로 사용해야 한다. 하지만 배열의 인덱스는 1,2,3 같은 숫자만 사용할 수 있다. A, B, C 와 같은 문자열은 배열의 인덱스로 사용할 수 없다. 문자열을 저장할 때 해시 인덱스를 사용해 보자.

문자를 숫자로 변경하는 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class StringHashMain {
    static final int CAPACITY = 10;

    public static void main(String[] args) {
        //char
        char charA = 'A';
        char charB = 'B';
        System.out.println(charA + " = " + (int)charA);
        System.out.println(charB + " = " + (int)charB);

        //hashCode
        System.out.println();
        System.out.println("hashCode A = " + hashCode("A"));    //hashCode A = 65
        System.out.println("hashCode B = " + hashCode("B"));    //hashCode B = 66
        System.out.println("hashCode C = " + hashCode("C"));    //hashCode C = 67
        System.out.println("hashCode AB = " + hashCode("AB"));  //hashCode AB = 131

        //hashIndex
        System.out.println("hashIndexA = " + hashIndex(hashCode("A")));
        System.out.println("hashIndexB = " + hashIndex(hashCode("B")));
        System.out.println("hashIndexC = " + hashIndex(hashCode("C")));
        System.out.println("hashIndexAB = " + hashIndex(hashCode("AB"))); //131 % 10 = 1
    }

    private static int hashIndex(int index) {
        return index % CAPACITY;
    }

    static int hashCode(String str) {
        char[] charArray = str.toCharArray();
        int sum = 0;
        for (char c : charArray) {
            sum += c;
        }
        return sum;
    }
}

//출력결과
A = 65
B = 66

hashCode A = 65
hashCode B = 66
hashCode C = 67
hashCode AB = 131
hashIndexA = 5
hashIndexB = 6
hashIndexC = 7
hashIndexAB = 1

컴퓨터는 ASCII 코드로 A → 65, B → 66 으로 인식한다. 그리고 문자를 저장할 때도 문자를 숫자로 변환해 저장한다.

https://www.ibm.com/docs/ko/sdse/6.4.0?topic=administering-ascii-characters-from-33-126

모든 문자는 고유한 숫자를 가진다.

Object.hashCode()

자바는 모든 객체가 자신만의 해시 코드를 표현할 수 있는 기능을 제공한다.

1
2
3
public class Object {
	public int hashCode();
}
  • 이 메서드를 재정의(오버라이딩) 해서 사용한다.
  • 객체의 참조값을 기반으로 해시 코드를 생성한다.
  • 객체의 인스턴스가 다르면 해시코드도 다르다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Member {

    private String id;

    public Member(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }

    @Override
    public boolean equals(Object object) {
        if (object == null || getClass() != object.getClass()) return false;
        Member member = (Member) object;
        return Objects.equals(id, member.id);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id); //이걸 기반으로 hashcode를 만듦
        //만약 없으면 object 즉 참조값으로 만든다
    }

    @Override
    public String toString() {
        return "Member{" +
                "id='" + id + '\'' +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class JavaHashCodeMain {
    public static void main(String[] args) {
        //Object의 기본 hashcode는 객체의 참조값을 기반으로 생성
        Object obj1 = new Object();
        Object obj2 = new Object();
        System.out.println("obj1.hashCode() = " + obj1.hashCode());
        System.out.println("obj2.hashCode() = " + obj2.hashCode());

        //각 클래스마다  hashCode를 이미 오버라이딩 해두었다
        Integer i = 10;
        String str = "A";
        String strAB = "AB";
        System.out.println("10.hashCode() = " + i.hashCode());
        System.out.println("str.hashCode() = " + str.hashCode());
        System.out.println("strAB.hashCode() = " + strAB.hashCode());

        //hashCode는 마이너스 값이 들어올 수 있다.
        System.out.println("-1.hashCode() = " + Integer.valueOf(-1).hashCode());

        //둘은 같나? 인스턴스는 다르지만 equals는 같다.
        Member member1 = new Member("idA");
        Member member2 = new Member("idA");

        //equals, hashCode를 오버라이딩 하지 않는 경우와 한 경우를 비교
        System.out.println("(member1 == member2) = " + (member1 == member2));
        System.out.println("(member1 equals member2) = " + (member1.equals(member2)));
        System.out.println("member1.hashCode() = " + member1.hashCode());
        System.out.println("member2.hashCode() = " + member2.hashCode());
    }
}

//출력결과
obj1.hashCode() = 2065951873
obj2.hashCode() = 1922154895
10.hashCode() = 10
str.hashCode() = 65
strAB.hashCode() = 2081
-1.hashCode() = -1
(member1 == member2) = false
(member1 equals member2) = true
member1.hashCode() = 104070
member2.hashCode() = 104070
  • Object 가 제공하는 hashCode()는 객체의 참조값을 해시 코드로 사용해 인스턴스마다 서로 다른 값을 반환한다 그로인해 obj1 obj2 는 서로 다른 해시코드를 반환한다.

기본 클래스의 해시 코드

Integer String 같은 기본 클래스들은 hashCode() 메서드를 재정의 해놓았다. 다라서 데이터의 값이 같으면 같은 해시 코드를 반환한다. 해시 코드의 경우 정수를 반환하기 때문에 마이너스 값이 나올 수 있다.

동일성(Identity) → == 연산자 사용, 두 객체의 참조가 동일한 객체를 가르키고 있는지 확인 동등성(Equality) → equals() 메서드를 사용해 두 객체가 논리적으로 동등한지 확인

따라서 회원 id 를 기반으로 동등성을 비교하도록 equals() 를 재정의 해야한다.

hashCode() 도 똑같이 id 가 같으면 논리적으로 같은 회원으로 표현할 수 있다. 따라서 회원 id 기반으로 해시 코드를 생성해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class MyHashSetV2 {
    static final int DEFAULT_INITIAL_CAPACITY = 10; //배열의 크기

    //모든 타입을 다 넣어야하니 Object
    private LinkedList<Object>[] bukets;

    private int size= 0;
    private int capacity = DEFAULT_INITIAL_CAPACITY;

    public MyHashSetV2() {
        initBuckets();
    }

    public MyHashSetV2(int initialCapacity) {
        this.capacity = initialCapacity;
        initBuckets();
    }

    private void initBuckets() {
        bukets = new LinkedList[capacity];
        for(int i = 0; i < capacity; i++) {
            bukets[i] = new LinkedList<>();
        }
    }

    public boolean add(Object value) {
        int hashIndex = hashIndex(value);
        LinkedList<Object> bucket = bukets[hashIndex]; //O(1)
        if(bucket.contains(value)) { //O(1) -> 평균적으로 데이터가 1,2개
            return false;
        }
        bucket.add(value);
        size++;
        return true;
    }

    //삭제
    public boolean remove(Object value) {
        int hashIndex = hashIndex(value);
        LinkedList<Object> bucket = bukets[hashIndex]; //O(1)
        boolean result = bucket.remove(value); //숫자를 넘기면 인덱스 위치를 지움, Integer.valueOf하면 값을 지움 //O(1)
        if(result) { //값이 지워졌으면
            size--;
            return true;
        }
        return false;
    }

    public boolean contains(Object searchValue) {
        int hashIndex = hashIndex(searchValue); //O(1)
        LinkedList<Object> bucket = bukets[hashIndex]; //O(1)
        //이 코드가 contains안에 있음 루프를 돌면서 찾음 (equals 사용처)
        /*for(Object object : bucket) {
            if(object.equals(searchValue)) {
                return true;
            }
        }
        return false;*/
        return bucket.contains(searchValue); //O(1) 데이터가 1개만
    }

    private int hashIndex(Object value) {
        //1. 해시 코드 구하기 (그냥 호출 ㅋㅋ)
        //2. 양수로 바꿔준다음(절댓값) 해시 인덱스 생성
        return Math.abs(value.hashCode()) % capacity;
        //hashCode의 결과로 음수가 나올 수 있다. abs()를 사용해서 마이너스를 제거
    }

    public int getSize() {
        return size;
    }

    @Override
    public String toString() {
        return "MyHashSetV2{" +
                "bukets=" + Arrays.toString(bukets) +
                ", size=" + size +
                ", capacity=" + capacity +
                '}';
    }
}
  • private LinkedList<Object>[] buckets 모든 타입을 저장할 수 있도록 Object 사용
  • 저장, 검색, 삭제 메서드의 매개변수도 변경

hashIndex()

  1. ObjecthashCode() 를 호출해 해시 코드를 찾음
  2. 찾은 해시 코드를 (capacity)로 나머지 연산 수행
  3. 해시 인덱스 반환

ObjecthashCode() 덕북에 모든 객체의 hashCode()를 구할 수 있다. 물론 다형성에 의해 오버라이딩 된 hashCode() 가 호출된다.

hashCode의 결과로 음수가 나올 수 있다. abs()를 사용해서 마이너스를 제거

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyhashSetV2Main1 {
    public static void main(String[] args) {
        MyHashSetV2 set = new MyHashSetV2(10);
        set.add("A");
        set.add("B");
        set.add("C");
        set.add("D");
        set.add("AB");
        set.add("SET");
        System.out.println(set);

        System.out.println("A.hashCode() = " + "A".hashCode());
        System.out.println("B.hashCode() = " + "B".hashCode());
        System.out.println("AB.hashCode() = " + "AB".hashCode());
        System.out.println("SET.hashCode() = " + "SET".hashCode());

        //검색
        String searchValue = "SET";
        boolean result = set.contains(searchValue);
        System.out.println("set.contains(" + searchValue + ") = " + result);
    }
}

//출력결과
MyHashSetV2{bukets=[[], [AB], [], [], [], [A], [B, SET], [C], [D], []], size=6, capacity=10}
A.hashCode() = 65
B.hashCode() = 66
AB.hashCode() = 2081
SET.hashCode() = 81986
set.contains(SET) = true

직접만든 객체 보관

Object 를 받으므로 Member 와 같은 객체도 보관할 수 있다.

객체가 hashCode() equals() 두 메서드를 반드시 구현해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MyHashSetV2Main2 {
  public static void main(String[] args) {
      MyHashSetV2 set = new MyHashSetV2(10);
      Member hi = new Member("hi");
      Member jpa = new Member("JPA"); //대문자 주의
      Member java = new Member("java");
      Member spring = new Member("spring");

      System.out.println("hi.hashCode() = " + hi.hashCode());
      System.out.println("jpa.hashCode() = " + jpa.hashCode());
      System.out.println("java.hashCode() = " + java.hashCode());
      System.out.println("spring.hashCode() = " + spring.hashCode()); //절댓값이라 상관무

      set.add(hi);
      set.add(jpa);
      set.add(java);
      set.add(spring);
      System.out.println(set);

      //검색
      Member searchVal = new Member("JPA");
      boolean result = set.contains(searchVal);
      System.out.println("set.contains(" + searchVal + ") = " + result);
  }
  
  
//출력결과
hi.hashCode() = 3329
jpa.hashCode() = 73659
java.hashCode() = 3254818
spring.hashCode() = -895679987
MyHashSetV2{bukets=[[], [], [], [], [], [], [], [Member{id='spring'}], [Member{id='java'}], [Member{id='hi'}, Member{id='JPA'}]], size=4, capacity=10}
set.contains(Member{id='JPA'}) = true
  
  • memberhashCode()id 값을 기반으로 재정의 하였다 따라서 반환된 해시 코드를 기반으로 해시 인덱스를 생성한다.
  • equals() 는 배열에 값이 여러개 있는 경우 비교하기 위해 사용된다

해시 자료 구조를 사용하는 경우 hashCode()equals()를 반드시 재정의 하자. 참고 → 자바가 제공하는 기본 클래스는 대부분 재정의 되어있다.

equals() 와 hashCode()의 중요성

해시 인덱스가 같아도 저장된 데이터는 다를 수 있다. 따라서 특정 인덱스에 데이터가 하나만 있어도 equals() 로 찾는 데이터가 맞는지 검증해야 한다.

클래스를 만들 때 hashCode()equals()를 재정의 하지 않으면 Object 에서 사용하는 hashCode()equals()를 사용한다. 하지만 Object가 기본으로 제공하는 기능은 단순 인스턴스의 참조를 기반으로 작동한다.

hashCode()equals() 모두 구현하지 않은 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MemberNoHashNoEq {

    private String id;

    public MemberNoHashNoEq(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }

    @Override
    public String toString() {
        return "MemberNoHashNoEq{" +
                "id='" + id + '\'' +
                '}';
    }
}

hashCode()equals()를 재정의 하지 않아, Object 에서 사용하는 hashCode()equals()를 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class HashAndEqualsMain1 {
    public static void main(String[] args) {
        MyHashSetV2 set = new MyHashSetV2(10);
        //m1,m2는 object의 기본기능을 사용하기 때문에 객체의 참조값을 기반으로 해시코드를 생성
        MemberNoHashNoEq m1 = new MemberNoHashNoEq("A");
        MemberNoHashNoEq m2 = new MemberNoHashNoEq("A");
        System.out.println("m1.hashCode() = " + m1.hashCode()); //m1.hashCode() = 1531448569
        System.out.println("m2.hashCode() = " + m2.hashCode()); //m2.hashCode() = 317574433
        System.out.println("m1.equals(m2): " + m1.equals(m2)); //m1.equals(m2): false

        //m1과 m2의 해시코드가 다르기 떄문에 다른위치에 각각 저장
        //id가 "A"로 같은 회원의 데이터가 중복등록 가능
        set.add(m1);
        set.add(m2);
        System.out.println(set);

        //검색실패
        MemberNoHashNoEq searchValue = new MemberNoHashNoEq("A");
        System.out.println("searchValue.hashCode() = " + searchValue.hashCode()); //searchValue.hashCode() = 1225358173
        boolean contains = set.contains(searchValue); //다른 위치에서 데이터를 찾게 되고
        System.out.println("contains = " + contains);// 검색 실패 false
    }
}

//출력결과
m1.hashCode() = 1531448569
m2.hashCode() = 317574433
m1.equals(m2): false
MyHashSetV2{bukets=[[], [], [], [MemberNoHashNoEq{id='A'}], [], [], [], [], [], [MemberNoHashNoEq{id='A'}]], size=2, capacity=10}
searchValue.hashCode() = 1225358173
contains = false

Object의 기본 기능을 사용하기 때문에 참조값을 기반으로 해시 코드를 생성한다. 따라서 실행할 때마다 값이 달라진다. 따라서 회원 idA 로 같은 데이터가 중복 저장된다.

검색을 하는 경우에도.

1
2
3
MemberNoHashNoEq searchValue = new MemberNoHashNoEq("A");
searchValue.hashCode() = 1225358173
contains = false

다른 위치에서 찾게 되고 검색에 실패하게 된다.

hashCode() 구현 equals() 미구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MemberOnlyHash {
    private String id;

    public MemberOnlyHash(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }

    /*@Override
    public boolean equals(Object object) {
        if (object == null || getClass() != object.getClass()) return false;
        MemberOnlyHash that = (MemberOnlyHash) object;
        return Objects.equals(id, that.id);
    }*/

    @Override
    public int hashCode() {
        return Objects.hashCode(id); //id를 시준으로 해시 코드 생성!!!
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class HashAndEqualsMain2 {
    public static void main(String[] args) {
        MyHashSetV2 set = new MyHashSetV2(10);
        MemberOnlyHash m1 = new MemberOnlyHash("A");
        MemberOnlyHash m2 = new MemberOnlyHash("A");
        System.out.println("m1.hashCode() = " + m1.hashCode()); //m1.hashCode() = 65
        System.out.println("m2.hashCode() = " + m2.hashCode()); //m2.hashCode() = 65
        /**
         * equals()를 재정의 하지 않아 Object의 equals()를 상속 받아 사용
         * 따라서 인스턴스의 참조값을 비교 그러므로 false
         */
        System.out.println("m1.equals(m2): " + m1.equals(m2)); //m1.equals(m2): false

        //참조값 확인 코드
        System.out.println("system.identityHashCode(m1) = " +System.identityHashCode(m1));
        System.out.println("system.identityHashCode(m2) = " +System.identityHashCode(m2));

        set.add(m1);
        set.add(m2);
        System.out.println(set);

        //검색실패
        MemberOnlyHash searchValue = new MemberOnlyHash("A");
        System.out.println("searchValue.hashCode() = " + searchValue.hashCode()); //searchValue.hashCode() = 1225358173
        boolean contains = set.contains(searchValue); //다른 위치에서 데이터를 찾게 되고
        System.out.println("contains = " + contains);// 검색 실패 false
    }
}

//실행결과
m1.hashCode() = 65
m2.hashCode() = 65
m1.equals(m2): false
system.identityHashCode(m1) = 1854731462
system.identityHashCode(m2) = 317574433
MyHashSetV2{bukets=[[], [], [], [], [], [collection.set.member.MemberOnlyHash@41, collection.set.member.MemberOnlyHash@41], [], [], [], []], size=2, capacity=10}
searchValue.hashCode() = 65
contains = false
  • hashCode 만 재정의 했기 때문에 같은 id를 사용하는 m1 m2 는 같은 해시 코드를 사용해 같은 해시 인덱스에 데이터가 저장된다
  • 하지만 add() 는 중복 데이터를 체크 하기 때문에 같은 데이터가 저장되면 안된다.
1
2
3
4
5
6
7
8
9
10
public boolean add(Object value) {
    int hashIndex = hashIndex(value);
    LinkedList<Object> bucket = bukets[hashIndex]; //O(1)
    if(bucket.contains(value)) { //O(1) -> 평균적으로 데이터가 1,2개
        return false;
    }
    bucket.add(value);
    size++;
    return true;
}
  • Objectequals()를 상속 받아 인스턴스의 참조값을 비교해 m1 m2는 비교에 실패한다
  • 따라서 중복 데이터가 없다 생각해 모두 저장한다.
  • 결과적으로 같은 회원 id가 중복 저장된다!
1
2
3
4
5
6
7
8
9
10
11
12
public boolean contains(Object searchValue) {
    int hashIndex = hashIndex(searchValue); //O(1)
    LinkedList<Object> bucket = bukets[hashIndex]; //O(1)
    //이 코드가 contains안에 있음 루프를 돌면서 찾음 (equals 사용처)
    /*for(Object object : bucket) {
        if(object.equals(searchValue)) {
            return true;
        }
    }
    return false;*/
    return bucket.contains(searchValue); //O(1) 데이터가 1개만
}
  • 검색도 마찬가지로 Objectequals()를 상속 받아 인스턴스의 참조값을 비교하여 실패한다.

제네릭과 인터페이스 도입

제네릭을 도입해 타입 안정성을 높이자

1
2
3
4
5
public interface MySet<E> {
    boolean add(E element);
    boolean remove(E value);
    boolean contains(E value);
}

인터페이스를 구현함으로써 해시 기반이 아니라 다른 자료 구조 기반의 Set도 만들 수 있게 되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class MyHashSetV3<E> implements MySet<E> {
    static final int DEFAULT_INITIAL_CAPACITY = 10; //배열의 크기

    //모든 타입을 다 넣어야하니 Object
    private LinkedList<E>[] bukets;

    private int size= 0;
    private int capacity = DEFAULT_INITIAL_CAPACITY;

    public MyHashSetV3() {
        initBuckets();
    }

    public MyHashSetV3(int initialCapacity) {
        this.capacity = initialCapacity;
        initBuckets();
    }

    private void initBuckets() {
        bukets = new LinkedList[capacity];
        for(int i = 0; i < capacity; i++) {
            bukets[i] = new LinkedList<>();
        }
    }

    public boolean add(E value) {
        int hashIndex = hashIndex(value);
        LinkedList<E> bucket = bukets[hashIndex]; //O(1)
        if(bucket.contains(value)) { //O(1) -> 평균적으로 데이터가 1,2개
            return false;
        }
        bucket.add(value);
        size++;
        return true;
    }

    //삭제
    public boolean remove(E value) {
        int hashIndex = hashIndex(value);
        LinkedList<E> bucket = bukets[hashIndex]; //O(1)
        boolean result = bucket.remove(value); //숫자를 넘기면 인덱스 위치를 지움, Integer.valueOf하면 값을 지움 //O(1)
        if(result) { //값이 지워졌으면
            size--;
            return true;
        }
        return false;
    }

    public boolean contains(E searchValue) {
        int hashIndex = hashIndex(searchValue); //O(1)
        LinkedList<E> bucket = bukets[hashIndex]; //O(1)
        //이 코드가 contains안에 있음 루프를 돌면서 찾음 (equals 사용처)
        /*for(Object object : bucket) {
            if(object.equals(searchValue)) {
                return true;
            }
        }
        return false;*/
        return bucket.contains(searchValue); //O(1) 데이터가 1개만
    }

    private int hashIndex(E value) {
        //1. 해시 코드 구하기 (그냥 호출 ㅋㅋ)
        //2. 양수로 바꿔준다음(절댓값) 해시 인덱스 생성
        return Math.abs(value.hashCode()) % capacity;
    }

    public int getSize() {
        return size;
    }

    @Override
    public String toString() {
        return "MyHashSetV3{" +
                "bukets=" + Arrays.toString(bukets) +
                ", size=" + size +
                ", capacity=" + capacity +
                '}';
    }
}

  • 기존 Object -> 타입 매개 변수 E 로 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
  MySet<String> set = new MyHashSetV3<>(10);
  set.add("A");
  set.add("B");
  set.add("C");
  System.out.println(set);

  //검색
  String searchValue = "A";
  boolean contains = set.contains(searchValue);
  System.out.println("set.contains(" + searchValue + ") " +contains);

  //제네릭 덕분에 타입 안정성이 높은 자료 구조를 만들 수 있었다.
}

//실행결과
MyHashSetV3{bukets=[[], [], [], [], [], [A], [B], [C], [], []], size=3, capacity=10}
set.contains(A) true

정리

hashCode()를 항상 재정의 하는 건 아니다. 하지만 해시 자료 구조를 사용하면 hashCode() equals() 를 반드시 함께 재정의 해야한다.



출처: 김영한의 실전 자바 - 중급 2편

This post is licensed under CC BY 4.0 by the author.