• Post Reply Bookmark Topic Watch Topic
  • New Topic
programming forums Java Mobile Certification Databases Caching Books Engineering Micro Controllers OS Languages Paradigms IDEs Build Tools Frameworks Application Servers Open Source This Site Careers Other all forums
this forum made possible by our volunteer staff, including ...
Marshals:
  • Campbell Ritchie
  • Paul Clapham
  • Ron McLeod
  • Bear Bibeault
  • Liutauras Vilda
Sheriffs:
  • Jeanne Boyarsky
  • Tim Cooke
  • Junilu Lacar
Saloon Keepers:
  • Tim Moores
  • Tim Holloway
  • Stephan van Hulst
  • Jj Roberts
  • Carey Brown
Bartenders:
  • salvin francis
  • Frits Walraven
  • Piet Souris

What difference exactly will come for this case if I override hashcode and equals?

 
Marshal
Posts: 71730
312
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I merged your stuff with the following thread. I hope that is okay by you.
 
Ranch Foreman
Posts: 2348
12
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Every object has a hashcode value. Using hashcode the address of element in memory (i.e the hash bucket)  is located.  If hashcode and equals method are overriden for a class the objects of such class  can be stored in hashset or as keys of hashMap.
Hashcode helps in determining that in which hash bucket would the element be stored in memory. But how exactly does hashcode help in determining this?
Thanks.
 
Campbell Ritchie
Marshal
Posts: 71730
312
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I thought we had told you that already. Maybe I was mistaken. Anyway, I think this post would fit better as a continuation of your previous thread.
 
Monica Shiralkar
Ranch Foreman
Posts: 2348
12
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Sure thanks. Let me go through the thread again to check on this.
 
Monica Shiralkar
Ranch Foreman
Posts: 2348
12
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
I went through the previous posts again.

To retrieve using hashMap/hashSet using object , it requires hashCode to locate that in which bucket the object is , and equals method to find the exact object.
If we override the equals method we convey the conditions based on which it should return true.E.g name should be equal and also the age should be equal.
Now to override equals , we have a mandatory requirement to override hashcode as well. I know that hashcode helps to determine that in which bucket the element is in, but how does overriding the hashCode help with regards to this?

Thanks.
 
Marshal
Posts: 26300
80
Eclipse IDE Firefox Browser MySQL Database
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
The rule is, as you already know: Your hashCode() method must be consistent with your equals() method.

Once you've done that, all of that business with buckets and so on is automatically taken care of. You don't need to worry about whether your hashCode() method will fill the right buckets, as long as it's consistent with equals() then it's all fine. All you need to know is that rule.

On the other hand: if you're working with class X which extends class Y, and you've written an equals() method for class Y, and if you need different "equals" logic for class X, then you have a different problem. Namely, your equals() method for class X won't be able to deal with comparing an X object to see if it's equal to a Y object. So you can't even write a proper equals() method. This makes the idea of writing a hashCode() method which is consistent with equals() a meaningless idea. (So yes, your question is not a meaningless question, you're just looking at the wrong things to figure out why there's a problem.)
 
Monica Shiralkar
Ranch Foreman
Posts: 2348
12
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
Thanks.

Paul Clapham wrote:The rule is, as you already know: Your hashCode() method must be consistent with your equals() method.



Let me put my question in a better way.
When we do not override the hashcode method, then also the object is having a hashcode value.
When we override the hashcode value, then also we give it a hashcode value.
So what exactly are we doing by overriding the hashcode method and giving it a hashcode value (different from what would have been its default hashcode value)?
 
Saloon Keeper
Posts: 12623
273
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator
If you don't override hashCode(), equal objects may be looked for in different buckets. This can result in weird behavior such as a set containing duplicate objects, or it reporting that it doesn't contain a certain object, even when it does.
 
Monica Shiralkar
Ranch Foreman
Posts: 2348
12
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Stephan van Hulst wrote:If you don't override hashCode(), equal objects may be looked for in different buckets.



But after overriding also, it is not sure that those will be in same bucket.

So, in both cases it may be in different buckets. That's what I understand based on the above 2 statements.
 
Campbell Ritchie
Marshal
Posts: 71730
312
  • Likes 1
  • Mark post as helpful
  • send pies
    Number of slices to send:
    Optional 'thank-you' note:
  • Quote
  • Report post to moderator

Paul Clapham wrote:. . . if you're working with class X which extends class Y, and you've written an equals() method for class Y, and if you need different "equals" logic for class X . . . you can't even write a proper equals() method. . . .

If you go through the standard resources, e.g. Joshua Bloch's Effective Java, or Angelika Lange's equals arrticles (partial list of her articles) or Odersky Spoon and Venners (artima.com), you will find that the problem occurs if you add fields to the subclass. If you don't add fields, you don't re‑override equals(), so all is well. If you add fields, or for any other reason re‑override equals, you will break part of its general contract, usually symmetry and/or transitivity. I think Joshua Bloch said somewhere that there are three things you are trying to do:-
  • 1: Add fields
  • 2: Maintain the general contract for equals()
  • 3: Obey the Liskov Substitution Principle (=LSP)
  • ...and you can achieve two out of three but it has never been possible to achieve all three. And here the quip

    Two out of three isn't bad.

    ...turns out to be really incorrect. The nearest you can get is adding fields but not letting equals() find out about them, which means

    don't re‑override equals().

    And if you look here, you will find that re‑overriding hashCode() to let it “find out” about the additional field will break the general contract for hashCode().

    * * * * * * * * * * * * * * * * * * *

    There are two ways you can use the hash code to decide the bucket number. But in the second case you may wish to do a bit of bit‑twiddling first.Remember the [unsigned right‑]shift operator >>> has a higher precedence than ^ which means bitwise exclusive or (=XOR). The idea of the shift is to bring the left part of the bit pattern to the right and then compare it to the part to its right. If the variability in the hash codes is confined to the left part of the bit pattern, that variability will be moved to the right. This process is called rehashing. Not sure, but I believe the Java9+ implementation of HashCode uses method 3.

    Both techniques create the buckets as elements in an Entry[] where Entry means a type similar to this. In both cases I shall use index to mean the bucket number, and I am calling the array values.
    Method 1: Use an array whose size is a prime number. Whenever the array is “full”, enlarge the array, so its length might follow a sequence of prime numbers like 19→37→73→139→etc.Advantages: No need for rehashing.
    Disadvantages: The % operator may give a negative result, so some technique is needed to change their sign. It isn't possible for % to return Integer.MIN_VALUE, so the poblems with that corner case won't apply. It is necessary to have a source of prime numbers when enlarging values.

    Method 2: Use an array whose size is a power of 2. Whenever the array is “full”, double its size 16→32→64→128→etc.Remember that minus has a higher precedence than bitwise and.
    Advantages: Doubling the array's length when it is “full” is very quick and easy in binary arithmetic. The - and & operators combined can be executed much faster than %, even if rehashing is needed. If the array has a positive length, that formula will never give a negative index.
    Disadvantages: Only works well for array lengths that can be expressed exactly as 2. Needs rehashing.

    The speed advantage of the bitwise operators even with rehashing means HashMap uses method 2. At least I think it does.
    Whenever the array is “full” according to the load factor chosen, all the Entries are removed from their buckets and put through the rehashing if used and entered into a larger array of buckets.
     
    Campbell Ritchie
    Marshal
    Posts: 71730
    312
    • Likes 1
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:. . . But after overriding also, it is not sure that those will be in same bucket. . . .

    Only if you have overridden hashCode() incorrectly. Remind yourself of the general contract for hashCode(). If you have overridden hashCode() correctly, then two keys returning true from equals() must always go into the same bucket.
     
    Campbell Ritchie
    Marshal
    Posts: 71730
    312
    • Likes 1
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:. . . what exactly are we doing by overriding the hashcode . . .?

    Making sure that two instances returning true from equals() have the same hash code.
    Trying as far as possible to make two instances returning false from equals() have different hash codes.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Campbell Ritchie wrote:

    Monica Shiralkar wrote:. . . But after overriding also, it is not sure that those will be in same bucket. . . .

    Only if you have overridden hashCode() incorrectly. Remind yourself of the general contract for hashCode(). If you have overridden hashCode() correctly, then two keys returning true from equals() must always go into the same bucket.



    Thanks.
    Below is my overridden hashCode method:



    This always gives the value 76175 as the hashCode.

    If it has to always return the value 76175 as it is doing, then how is it different than hardCoding the hashCode?
     
    Stephan van Hulst
    Saloon Keeper
    Posts: 12623
    273
    • Likes 1
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
    Here is an extremely simplified implementation of a hash table:

    The table is really just an array of buckets, with each bucket implemented by a TreeMap (as per Jesse's suggestion, and not as I said earlier, a LinkedList). The trick is in the getBucket() method. The table calculates a bucket index from the hash code of the key. In an actual HashMap this calculation is MUCH more complex in order to distribute keys over the table a bit more uniformly, but the overall concept is the same.

    Keys with the same hash code will ALWAYS end up in the same bucket. This guarantee is necessary because otherwise you wouldn't be able to put key "A" into a bucket and then later if you perform table.get("A"), find the same bucket again.

    The inverse is NOT true. Keys with a different hash code will not necessarily be put in different buckets. This application demonstrates that:

    As you can see, one bucket contains two keys ("C" and "F") while another bucket contains no keys at all. This is NOT because "C" and "F" have the same hash code, but because the hash codes of "C" and "F" just coincidentally happen to map to the same bucket index.

    This is not a disaster though. For bigger tables and for lower load factors, entries will generally be distributed over the table quite well, and each bucket will contain no more than a few entries.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Campbell Ritchie wrote:
    Trying as far as possible to make two instances returning false from equals() have different hash codes.



    What is meant here by "as far as possible"?

     
    Stephan van Hulst
    Saloon Keeper
    Posts: 12623
    273
    • Likes 2
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:This always gives the value 76175 as the hashCode.


    Only for a certain name and age. For a different name and age it will return a different hash code.

    If it has to always return the value 76175 as it is doing, then how is it different than hardCoding the hashCode?


    Because an object that has a different name and age would NOT return 76175.

    What is meant here by "as far as possible"?


    Most classes have an almost infinite amount of distinct objects you can create from them. For such classes, you can't guarantee that different object will have different hash codes. However, you must try make a best effort to make two different objects return a different hash code. Here is an example that DOES NOT make a best effort:

    Objects that have a different age will return a different hash code, but all objects that have the same age will return the same hash code, even if they have different names. This will cause all of them to go into the same bucket, even if they have different names. This will severely limit the performance offered by a hash table.
     
    Campbell Ritchie
    Marshal
    Posts: 71730
    312
    • Likes 1
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
    I tried exactly the same formula as you showed and didn't get the same hash codes every time:-

    jshell> System.out.printf("name 1: %h%nname 2: %h%n", new Name("Monica", 29), new Name("Campbell", 97));
    name 1: 89b4f75f
    name 2: fb83daa4

    You can't have tested your hash code method very well. You are using the same values for both fields and if you have two distinct objects with the same state, you should get the same hash codes.
     
    Campbell Ritchie
    Marshal
    Posts: 71730
    312
    • Likes 1
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:. . . What is meant here by "as far as possible"?

    The same as, “as far as possible,” usually means.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Campbell Ritchie wrote:The same as, “as far as possible,” usually means.



    If one has to do something , there will be a way to do it.
    If one has not to do something, there will be a way not to do it.
    But if someone has to do something "as far as possible" (through programming), then I am trying to understand how?
     
    Campbell Ritchie
    Marshal
    Posts: 71730
    312
    • Likes 1
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
    You want to make as many non‑equal instances return different hash codes . . . . as possible.
     
    Paul Clapham
    Marshal
    Posts: 26300
    80
    Eclipse IDE Firefox Browser MySQL Database
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:But if someone has to do something "as far as possible" (through programming), then I am trying to understand how?



    If you're looking for a hard and fast answer to a question which obviously doesn't have a hard and fast answer, then clearly you aren't going to get an answer.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Stephan van Hulst wrote:
    Only for a certain name and age. For a different name and age it will return a different hash code.
    .


    Understood. Thanks
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
    In case of HashMap, once we have implemented hashcode and equals methods , we can create object of such class and pass it in get method like to retrieve the value of this key.
    What about HashSet?
     
    Bartender
    Posts: 2780
    135
    Google Web Toolkit Eclipse IDE Java
    • Likes 2
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
    I would like to share a short analogy with regards to hashcode and equals.

    Let's say you have a office cabinet having 2 drawers. You have 100 files each having a person's name on the tab. Now, you want to arrange them for easy access. What would you do ?
    A common and simple strategy would be to simply store them as "A-M" in drawer 1 and "N-Z" in drawer 2. Now let's say you have 26 drawers, a faster strategy would be to assign a single letter to each drawer. At the max, you would have to search through 4 files given a set of 100 evenly spaced out items. If a person named "John" wants to access his file, you would simply head to the drawer named "J" and scan through each of the files one by one till you reach "John". To scale this problem to easily search across 1000 files, one might suggest increasing the drawers and having it grouped year wise (2020, 2019, 2018, etc..)

    If you crammed all files into a single drawer then you are going to spend a lot of time searching through each file. This is the case of constant returned hashcode.

    When the total files increase above a given load, you will drop an email to higher management and they hesitantly agree. Of course, once the number of drawers have increased, you would spend an entire day shuffling files to their new drawers, but the effort would be worth it !!

    The HashMap does something similar internally. The drawers would be the buckets for storing the objects and act of scanning through each one to check the name is analogous to running the equals on each object.
    (although the above statement is not entirely true, the HashMap does store the objects in a linked Node having a "next" reference. It can even use a TreeNode having a "parent", "left", "right", "prev")

    I hope the analogy would clear things for you.
     
    salvin francis
    Bartender
    Posts: 2780
    135
    Google Web Toolkit Eclipse IDE Java
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:...What about HashSet?


    A HashSet, as far as I remember is a wrapper for a HashMap with your element as the key and a constant object as a value.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    salvin francis wrote:I would like to share a short analogy with regards to hashcode and equals.


    Thanks.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    salvin francis wrote:
    A HashSet, as far as I remember is a wrapper for a HashMap with your element as the key and a constant object as a value.



    For HashMap:

    We override hashcode and equals so that we can store such objects in the hashMap and call the below kind of code successfully


    For HashSet:
    Similarly, we override hashcode and equals so that we can store such objects in the hashSet and call what kind of code successfully ?
     
    Campbell Ritchie
    Marshal
    Posts: 71730
    312
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
     
    Stephan van Hulst
    Saloon Keeper
    Posts: 12623
    273
    • Likes 1
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
    What does it matter? Isn't it enough that HashSet's internal consistency depends on the correct implementation of equals() and hashCode(), regardless of what method you call?
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Campbell Ritchie wrote:


    Thanks.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Stephan van Hulst wrote:What does it matter? Isn't it enough that HashSet's internal consistency depends on the correct implementation of equals() and hashCode(), regardless of what method you call?


    And if we dont implement hashCode and equals methods, retrieval from hashSet will not be fast. Is that correct?
     
    Stephan van Hulst
    Saloon Keeper
    Posts: 12623
    273
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
    What do you mean by "don't implement"? If you don't override equals and hashCode at all, every object will be considered distinct. This is a semantics issue, not a performance issue.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Stephan van Hulst wrote:What do you mean by "don't implement"? If you don't override equals and hashCode at all, every object will be considered distinct. This is a semantics issue, not a performance issue.



    Sorry. I mean "do not override".


    I was thinking that in case of hashSet, if we do not override hashcode and equals method then the below problems will come:

    1) You cannot use code such as  since every object is different.
    2)  Even if we do not use the above kind of code, the performance on retrieving from hashSet will not be fast.

    But, you have corrected me , saying that the 2nd point listed above is not right and instead there will be semantics issue. What exactly will this "semantics issue" cause (if not for slow performance as you said)?
     
    Paul Clapham
    Marshal
    Posts: 26300
    80
    Eclipse IDE Firefox Browser MySQL Database
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:1) You cannot use code such as  since every object is different.
    2)  Even if we do not use the above kind of code, the performance on retrieving from hashSet will not be fast.

    But, you have corrected me , saying that the 2nd point listed above is not right and instead there will be semantics issue. What exactly will this "semantics issue" cause (if not for slow performance as you said)?



    The "semantics" issue is that you're creating a new Employee object with certain parameters, assuming that it will be equal to another Employee object with the same parameters. But it won't. The two objects are different objects and so they are not equal. So using one as a key to a HashMap for the purpose of finding the other one isn't going to work.

    In other words the problem isn't that it works slowly, the problem is that it doesn't work at all.
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Paul Clapham wrote:
    The "semantics" issue is that you're creating a new Employee object with certain parameters, assuming that it will be equal to another Employee object with the same parameters. But it won't. The two objects are different objects and so they are not equal. So using one as a key to a HashMap for the purpose of finding the other one isn't going to work.

    In other words the problem isn't that it works slowly, the problem is that it doesn't work at all.



    I agree that "using one as a key to a HashMap for the purpose of finding the other one isn't going to work" neither in hashMap nor in hashSet.

    My question is that in case of HashSet , one problem is that using key in contains method will not work. If we leave this problem for a moment for the case where one is not using any contains method and all one is doing is retrieving elements from the hashSet. Will there be any problem there ?
     
    Paul Clapham
    Marshal
    Posts: 26300
    80
    Eclipse IDE Firefox Browser MySQL Database
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:My question is that in case of HashSet , one problem is that using key in contains method will not work. If we leave this problem for a moment for the case where one is not using any contains method and all one is doing is retrieving elements from the hashSet. Will there be any problem there?



    What method did you propose to use to get elements out of the set?
     
    Monica Shiralkar
    Ranch Foreman
    Posts: 2348
    12
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator
    I was thinking of using Iterator like the below:


     
    Campbell Ritchie
    Marshal
    Posts: 71730
    312
    • Likes 1
    • Mark post as helpful
    • send pies
      Number of slices to send:
      Optional 'thank-you' note:
    • Quote
    • Report post to moderator

    Monica Shiralkar wrote:. . . You cannot use code such as  since every object is different.

    That is a semantics issue, as Stephan said. Of course you can use such code; that method call will however return false on spec.

    2)  Even if we do not use the above kind of code, the performance on retrieving from hashSet will not be fast. . . .

    Not so: as Stephan said it doesn't affect retrieval speed.

    That is something every developer ought to know already. You have hinted that you already know what the semantics issue is: that such a method call will always return false. That is because you haven't updated the semantics of equals()/hashCode() from what occurs in Object. What have you tried? Have you tried populating sets with millions of elements and have you timed anything? Have you looked for duplicates in a set? You will learn a lot more by putting the effort into it yourself.
     
    It sure was nice of your sister to lend us her car. Let's show our appreciation by sharing this tiny ad:
    Building a Better World in your Backyard by Paul Wheaton and Shawn Klassen-Koop
    https://coderanch.com/wiki/718759/books/Building-World-Backyard-Paul-Wheaton
    reply
      Bookmark Topic Watch Topic
    • New Topic